Merged with branch 'eric7' in order to track these changes. server

Fri, 23 Feb 2024 10:46:46 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 23 Feb 2024 10:46:46 +0100
branch
server
changeset 10601
a8775ecd25dc
parent 10599
36b8c7115c32 (diff)
parent 10600
954bd7d48910 (current diff)
child 10602
a767e9c400fa

Merged with branch 'eric7' in order to track these changes.

src/eric7/Preferences/__init__.py file | annotate | diff | comparison | revisions
src/eric7/icons/oxygen/dialog-cancel.png file | annotate | diff | comparison | revisions
src/eric7/icons/oxygen/dialog-ok.png file | annotate | diff | comparison | revisions
src/eric7/icons/oxygen/dirNew.png file | annotate | diff | comparison | revisions
src/eric7/icons/oxygen/fileSaveAsRemote.png file | annotate | diff | comparison | revisions
src/eric7/icons/oxygen/open-remote.png file | annotate | diff | comparison | revisions
src/eric7/icons/oxygen/preferences-eric-server.png file | annotate | diff | comparison | revisions
src/eric7/icons/oxygen/projectOpen-remote.png file | annotate | diff | comparison | revisions
src/eric7/icons/oxygen/projectSaveAs-remote.png file | annotate | diff | comparison | revisions
--- a/eric7.epj	Fri Feb 23 10:43:39 2024 +0100
+++ b/eric7.epj	Fri Feb 23 10:46:46 2024 +0100
@@ -630,6 +630,7 @@
       "src/eric7/Preferences/ConfigurationPages/EditorSyntaxPage.ui",
       "src/eric7/Preferences/ConfigurationPages/EditorTypingPage.ui",
       "src/eric7/Preferences/ConfigurationPages/EmailPage.ui",
+      "src/eric7/Preferences/ConfigurationPages/EricServerPage.ui",
       "src/eric7/Preferences/ConfigurationPages/GraphicsPage.ui",
       "src/eric7/Preferences/ConfigurationPages/HelpDocumentationPage.ui",
       "src/eric7/Preferences/ConfigurationPages/HelpViewersPage.ui",
@@ -709,6 +710,9 @@
       "src/eric7/QtHelpInterface/QtHelpDocumentationConfigurationDialog.ui",
       "src/eric7/QtHelpInterface/QtHelpDocumentationSelectionDialog.ui",
       "src/eric7/QtHelpInterface/QtHelpDocumentationSettingsWidget.ui",
+      "src/eric7/RemoteServerInterface/EricServerConnectionDialog.ui",
+      "src/eric7/RemoteServerInterface/EricServerFileDialog.ui",
+      "src/eric7/RemoteServerInterface/EricServerProfilesDialog.ui",
       "src/eric7/Snapshot/SnapWidget.ui",
       "src/eric7/SqlBrowser/SqlBrowserWidget.ui",
       "src/eric7/SqlBrowser/SqlConnectionDialog.ui",
@@ -1188,6 +1192,7 @@
       "src/eric7/EricGraphics/EricGraphicsView.py",
       "src/eric7/EricGraphics/__init__.py",
       "src/eric7/EricGui/EricAction.py",
+      "src/eric7/EricGui/EricFileIconProvider.py",
       "src/eric7/EricGui/EricGenericDiffHighlighter.py",
       "src/eric7/EricGui/EricOverrideCursor.py",
       "src/eric7/EricGui/EricPixmapCache.py",
@@ -1918,6 +1923,7 @@
       "src/eric7/Preferences/ConfigurationPages/EditorSyntaxPage.py",
       "src/eric7/Preferences/ConfigurationPages/EditorTypingPage.py",
       "src/eric7/Preferences/ConfigurationPages/EmailPage.py",
+      "src/eric7/Preferences/ConfigurationPages/EricServerPage.py",
       "src/eric7/Preferences/ConfigurationPages/GraphicsPage.py",
       "src/eric7/Preferences/ConfigurationPages/HelpDocumentationPage.py",
       "src/eric7/Preferences/ConfigurationPages/HelpViewersPage.py",
@@ -2108,6 +2114,20 @@
       "src/eric7/QtHelpInterface/QtHelpDocumentationSettingsWidget.py",
       "src/eric7/QtHelpInterface/QtHelpSchemeHandler.py",
       "src/eric7/QtHelpInterface/__init__.py",
+      "src/eric7/RemoteServer/EricRequestCategory.py",
+      "src/eric7/RemoteServer/EricServer.py",
+      "src/eric7/RemoteServer/EricServerCoverageRequestHandler.py",
+      "src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py",
+      "src/eric7/RemoteServer/EricServerFileSystemRequestHandler.py",
+      "src/eric7/RemoteServer/__init__.py",
+      "src/eric7/RemoteServerInterface/EricServerConnectionDialog.py",
+      "src/eric7/RemoteServerInterface/EricServerCoverageInterface.py",
+      "src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py",
+      "src/eric7/RemoteServerInterface/EricServerFileDialog.py",
+      "src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py",
+      "src/eric7/RemoteServerInterface/EricServerInterface.py",
+      "src/eric7/RemoteServerInterface/EricServerProfilesDialog.py",
+      "src/eric7/RemoteServerInterface/__init__.py",
       "src/eric7/Sessions/SessionFile.py",
       "src/eric7/Sessions/__init__.py",
       "src/eric7/Snapshot/SnapWidget.py",
@@ -2493,6 +2513,7 @@
       "src/eric7/eric7_qregularexpression.pyw",
       "src/eric7/eric7_re.py",
       "src/eric7/eric7_re.pyw",
+      "src/eric7/eric7_server.py",
       "src/eric7/eric7_shell.py",
       "src/eric7/eric7_shell.pyw",
       "src/eric7/eric7_snap.py",
--- a/pyproject.toml	Fri Feb 23 10:43:39 2024 +0100
+++ b/pyproject.toml	Fri Feb 23 10:46:46 2024 +0100
@@ -101,6 +101,7 @@
 eric7_api = "eric7.eric7_api:main"
 eric7_doc = "eric7.eric7_doc:main"
 eric7_post_install = "eric7.eric7_post_install:main"
+eric7_server =  "eric7.eric7_server:main"
 
 [project.gui-scripts]
 eric7_browser = "eric7.eric7_browser:main"
--- a/src/eric7/DataViews/CodeMetrics.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/DataViews/CodeMetrics.py	Fri Feb 23 10:46:46 2024 +0100
@@ -24,6 +24,8 @@
 from dataclasses import dataclass
 
 from eric7 import Utilities
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities
 
 KEYWORD = token.NT_OFFSET + 1
 COMMENT = tokenize.COMMENT
@@ -226,7 +228,13 @@
     @rtype SourceStat
     """
     try:
-        text = Utilities.readEncodedFile(filename)[0]
+        if FileSystemUtilities.isRemoteFileName(filename):
+            remotefsInterface = (
+                ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+            )
+            text = remotefsInterface.readEncodedFile(filename)[0]
+        else:
+            text = Utilities.readEncodedFile(filename)[0]
     except (OSError, UnicodeError):
         return SourceStat()
 
--- a/src/eric7/DataViews/CodeMetricsDialog.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/DataViews/CodeMetricsDialog.py	Fri Feb 23 10:46:46 2024 +0100
@@ -56,6 +56,10 @@
 
         self.cancelled = False
 
+        self.__remotefsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
         self.__menu = QMenu(self)
         self.__menu.addAction(self.tr("Collapse All"), self.__resultCollapse)
         self.__menu.addAction(self.tr("Expand All"), self.__resultExpand)
@@ -153,15 +157,26 @@
         loc = QLocale()
         if isinstance(fn, list):
             files = fn
-        elif os.path.isdir(fn):
+        elif FileSystemUtilities.isRemoteFileName(
+            fn
+        ) and self.__remotefsInterface.isdir(fn):
+            files = [
+                FileSystemUtilities.remoteFileName(f)
+                for f in self.__remotefsInterface.direntries(fn, True, "*.py", False)
+            ]
+        elif FileSystemUtilities.isPlainFileName(fn) and os.path.isdir(fn):
             files = FileSystemUtilities.direntries(fn, True, "*.py", False)
         else:
             files = [fn]
         files.sort()
         # check for missing files
         for f in files[:]:
-            if not os.path.exists(f):
-                files.remove(f)
+            if FileSystemUtilities.isRemoteFileName(f):
+                if not self.__remotefsInterface.exists(f):
+                    files.remove(f)
+            else:
+                if not os.path.exists(f):
+                    files.remove(f)
 
         self.checkProgress.setMaximum(len(files))
         QApplication.processEvents()
--- a/src/eric7/DataViews/PyCoverageDialog.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/DataViews/PyCoverageDialog.py	Fri Feb 23 10:46:46 2024 +0100
@@ -25,6 +25,9 @@
 
 from eric7.EricWidgets import EricMessageBox
 from eric7.EricWidgets.EricApplication import ericApp
+from eric7.RemoteServerInterface.EricServerCoverageInterface import (
+    EricServerCoverageError,
+)
 from eric7.SystemUtilities import FileSystemUtilities
 
 from .Ui_PyCoverageDialog import Ui_PyCoverageDialog
@@ -57,7 +60,6 @@
         self.resultList.headerItem().setText(self.resultList.columnCount(), "")
 
         self.cancelled = False
-        self.path = "."
         self.reload = False
 
         self.excludeList = ["# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]"]
@@ -78,6 +80,11 @@
         self.resultList.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
         self.resultList.customContextMenuRequested.connect(self.__showContextMenu)
 
+        # eric-ide server interface
+        self.__serverCoverageInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("Coverage")
+        )
+
     def __format_lines(self, lines):
         """
         Private method to format a list of integers into string by coalescing
@@ -143,7 +150,7 @@
         @param coverage percent of coverage
         @type int
         @param excluded list of excluded lines
-        @type str
+        @type list of int
         @param missing list of lines without coverage
         @type str
         """
@@ -154,7 +161,7 @@
                 str(statements),
                 str(executed),
                 "{0:.0f}%".format(coverage),
-                excluded,
+                excluded and self.__format_lines(excluded) or "",
                 missing,
             ],
         )
@@ -194,18 +201,19 @@
 
         if isinstance(fn, list):
             files = fn
-            self.path = os.path.dirname(cfn)
-        elif os.path.isdir(fn):
+        elif FileSystemUtilities.isRemoteFileName(
+            self.cfn
+        ) and self.__remotefsInterface.isdir(fn):
+            files = [
+                FileSystemUtilities.remoteFileName(f)
+                for f in self.__remotefsInterface.direntries(fn, True, "*.py", False)
+            ]
+        elif FileSystemUtilities.isPlainFileName(self.cfn) and os.path.isdir(fn):
             files = FileSystemUtilities.direntries(fn, True, "*.py", False)
-            self.path = fn
         else:
             files = [fn]
-            self.path = os.path.dirname(cfn)
         files.sort()
 
-        cover = Coverage(data_file=self.cfn)
-        cover.load()
-
         # set the exclude pattern
         self.excludeCombo.clear()
         self.excludeCombo.addItems(self.excludeList)
@@ -217,7 +225,23 @@
         total_executed = 0
         total_exceptions = 0
 
-        cover.exclude(self.excludeList[0])
+        if FileSystemUtilities.isRemoteFileName(self.cfn):
+            ok, error = self.__serverCoverageInterface.loadCoverageData(
+                self.cfn, self.excludeList[0]
+            )
+            if not ok:
+                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),
+                )
+        else:
+            cover = Coverage(data_file=self.cfn)
+            cover.load()
+            cover.exclude(self.excludeList[0])
 
         try:
             # disable updates of the list for speed
@@ -231,18 +255,28 @@
                     return
 
                 try:
-                    statements, excluded, missing, readable = cover.analysis2(file)[1:]
-                    readableEx = excluded and self.__format_lines(excluded) or ""
+                    if FileSystemUtilities.isRemoteFileName(self.cfn):
+                        (
+                            file,
+                            statements,
+                            excluded,
+                            missing,
+                            readable,
+                        ) = self.__serverCoverageInterface.analyzeFile(file)
+                    else:
+                        statements, excluded, missing, readable = cover.analysis2(file)[
+                            1:
+                        ]
                     n = len(statements)
                     m = n - len(missing)
                     pc = 100.0 * m / n if n > 0 else 100.0
                     self.__createResultItem(
-                        file, str(n), str(m), pc, readableEx, readable
+                        file, str(n), str(m), pc, excluded, readable
                     )
 
                     total_statements += n
                     total_executed += m
-                except CoverageException:
+                except (CoverageException, EricServerCoverageError):
                     total_exceptions += 1
 
                 self.checkProgress.setValue(progress)
--- a/src/eric7/DataViews/PyProfileDialog.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/DataViews/PyProfileDialog.py	Fri Feb 23 10:46:46 2024 +0100
@@ -22,7 +22,8 @@
 )
 
 from eric7.EricWidgets import EricMessageBox
-from eric7.SystemUtilities import PythonUtilities
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities, PythonUtilities
 
 from .Ui_PyProfileDialog import Ui_PyProfileDialog
 
@@ -104,6 +105,11 @@
         self.summaryList.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
         self.summaryList.customContextMenuRequested.connect(self.__showContextMenu)
 
+        # eric-ide server interface
+        self.__serverFsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
     def __createResultItem(
         self,
         calls,
@@ -265,7 +271,10 @@
         self.basename = os.path.splitext(pfn)[0]
 
         fname = "{0}.profile".format(self.basename)
-        if not os.path.exists(fname):
+        if (
+            FileSystemUtilities.isRemoteFileName(fname)
+            and not self.__serverFsInterface.exists(fname)
+        ) or (FileSystemUtilities.isPlainFileName(fname) and not os.path.exists(fname)):
             EricMessageBox.warning(
                 self,
                 self.tr("Profile Results"),
@@ -277,8 +286,12 @@
             self.close()
             return
         try:
-            with open(fname, "rb") as f:
-                self.stats = pickle.load(f)  # secok
+            if FileSystemUtilities.isRemoteFileName(fname):
+                data = self.__serverFsInterface.readFile(fname)
+                self.stats = pickle.loads(data)
+            else:
+                with open(fname, "rb") as f:
+                    self.stats = pickle.load(f)  # secok
         except (EOFError, OSError, pickle.PickleError):
             EricMessageBox.critical(
                 self,
@@ -291,7 +304,10 @@
             self.close()
             return
 
-        self.file = fn
+        if FileSystemUtilities.isRemoteFileName(fname):
+            self.file = FileSystemUtilities.plainFileName(fn)
+        else:
+            self.file = fn
         self.__populateLists()
         self.__finish()
 
--- a/src/eric7/DebugClients/Python/DebugClientBase.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/DebugClients/Python/DebugClientBase.py	Fri Feb 23 10:46:46 2024 +0100
@@ -1106,6 +1106,7 @@
         self.eventExit = False
         self.pollingDisabled = disablePolling
         selectErrors = 0
+        self.rawLine = ""
 
         while not self.eventExit:
             wrdy = []
--- a/src/eric7/Debugger/DebugServer.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Debugger/DebugServer.py	Fri Feb 23 10:46:46 2024 +0100
@@ -18,6 +18,7 @@
 from eric7 import Preferences
 from eric7.EricWidgets import EricMessageBox
 from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities
 
 from . import DebugClientCapabilities
 from .BreakPointModel import BreakPointModel
@@ -241,6 +242,8 @@
         self.__autoClearShell = False
         self.__forProject = False
 
+        self.__ericServerDebugging = False
+
         self.clientClearBreak.connect(self.__clientClearBreakPoint)
         self.clientClearWatch.connect(self.__clientClearWatchPoint)
         self.newConnection.connect(self.__newConnection)
@@ -492,28 +495,32 @@
         forProject=False,
         runInConsole=False,
         venvName="",
-        workingDir=None,
+        workingDir="",
         configOverride=None,
+        startRemote=None,
     ):
         """
         Public method to start a debug client.
 
-        @param unplanned flag indicating that the client has died
-        @type bool
-        @param clType type of client to be started
-        @type str
-        @param forProject flag indicating a project related action
-        @type bool
+        @param unplanned flag indicating that the client has died (defaults to True)
+        @type bool (optional)
+        @param clType type of client to be started (defaults to None)
+        @type str (optional)
+        @param forProject flag indicating a project related action (defaults to False)
+        @type bool (optional)
         @param runInConsole flag indicating to start the debugger in a
-            console window
-        @type bool
-        @param venvName name of the virtual environment to be used
-        @type str
-        @param workingDir directory to start the debugger client in
-        @type str
-        @param configOverride dictionary containing the global config override
-            data
-        @type dict
+            console window (defaults to False)
+        @type bool (optional)
+        @param venvName name of the virtual environment to be used (defaults to "")
+        @type str (optional)
+        @param workingDir directory to start the debugger client in (defaults to "")
+        @type str (optional)
+        @param configOverride dictionary containing the global config override data
+            (defaults to None)
+        @type dict (optional)
+        @param startRemote flag indicating to start the client via an eric-ide server
+            (defaults to None)
+        @type bool (optional)
         """
         self.running = False
 
@@ -560,6 +567,7 @@
                         self.__originalPathString,
                         workingDir=workingDir,
                         configOverride=configOverride,
+                        startRemote=startRemote,
                     )
                 else:
                     (
@@ -573,6 +581,7 @@
                         self.__originalPathString,
                         workingDir=workingDir,
                         configOverride=configOverride,
+                        startRemote=startRemote,
                     )
             else:
                 (
@@ -586,6 +595,7 @@
                     self.__originalPathString,
                     workingDir=workingDir,
                     configOverride=configOverride,
+                    startRemote=startRemote,
                 )
 
             if self.clientProcess:
@@ -603,6 +613,14 @@
                 elif self.__autoClearShell:
                     self.__autoClearShell = False
                     self.remoteBanner()
+            elif startRemote:
+                self.__ericServerDebugging = True
+                if self.lastClientType != self.clientType:
+                    self.lastClientType = self.clientType
+                    self.remoteBanner()
+                elif self.__autoClearShell:
+                    self.__autoClearShell = False
+                    self.remoteBanner()
             else:
                 if clType and self.lastClientType:
                     self.__setClientType(self.lastClientType)
@@ -726,11 +744,20 @@
                 if (fn, lineno) in self.__reportedBreakpointIssues:
                     self.__reportedBreakpointIssues.remove((fn, lineno))
 
-                self.remoteBreakpoint(debuggerId, fn, lineno, True, cond, temp)
-                if not enabled:
-                    self.__remoteBreakpointEnable(debuggerId, fn, lineno, False)
-                if ignorecount:
-                    self.__remoteBreakpointIgnore(debuggerId, fn, lineno, ignorecount)
+                if (
+                    self.__ericServerDebugging
+                    and FileSystemUtilities.isRemoteFileName(fn)
+                ) or (
+                    not self.__ericServerDebugging
+                    and FileSystemUtilities.isPlainFileName(fn)
+                ):
+                    self.remoteBreakpoint(debuggerId, fn, lineno, True, cond, temp)
+                    if not enabled:
+                        self.__remoteBreakpointEnable(debuggerId, fn, lineno, False)
+                    if ignorecount:
+                        self.__remoteBreakpointIgnore(
+                            debuggerId, fn, lineno, ignorecount
+                        )
 
     def __makeWatchCondition(self, cond, special):
         """
@@ -901,12 +928,13 @@
 
     def isClientProcessUp(self):
         """
-        Public method to check, if the debug client process is up.
+        Public method to check, if the debug client process is up or we are
+        doing debugging via the eric-ide server.
 
         @return flag indicating a running debug client process
         @rtype bool
         """
-        return self.clientProcess is not None
+        return self.clientProcess is not None or self.__ericServerDebugging
 
     def __newConnection(self):
         """
@@ -1088,6 +1116,7 @@
             runInConsole=runInConsole,
             venvName=venvName,
             configOverride=configOverride,
+            startRemote=FileSystemUtilities.isRemoteFileName(fn),
         )
 
         self.setCallTraceEnabled("", enableCallTrace)
@@ -1178,6 +1207,7 @@
             runInConsole=runInConsole,
             venvName=venvName,
             configOverride=configOverride,
+            startRemote=FileSystemUtilities.isRemoteFileName(fn),
         )
 
         self.remoteEnvironment(env)
@@ -1260,6 +1290,7 @@
             runInConsole=runInConsole,
             venvName=venvName,
             configOverride=configOverride,
+            startRemote=FileSystemUtilities.isRemoteFileName(fn),
         )
 
         self.remoteEnvironment(env)
@@ -1342,6 +1373,7 @@
             runInConsole=runInConsole,
             venvName=venvName,
             configOverride=configOverride,
+            startRemote=FileSystemUtilities.isRemoteFileName(fn),
         )
 
         self.remoteEnvironment(env)
@@ -1910,6 +1942,7 @@
             self.signalClientOutput(self.tr("\nNot connected\n"))
             self.signalClientStatement(False, "")
         self.running = False
+        self.__ericServerDebugging = False
 
     def signalClientClearBreak(self, filename, lineno, debuggerId):
         """
@@ -2223,3 +2256,14 @@
         except KeyError:
             # The project object is not present
             return ""
+
+    def getEricServerEnvironmentString(self):
+        """
+        Public method to get the string for an eric-ide server environment.
+
+        @return string for the eric-ide server environment
+        @rtype str
+        """
+        # TODO: make this more elaborate once server environments definitions
+        #       are supported
+        return "eric-ide Server"
--- a/src/eric7/Debugger/DebugUI.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Debugger/DebugUI.py	Fri Feb 23 10:46:46 2024 +0100
@@ -2048,6 +2048,13 @@
                     self.clientType = editor.determineFileType()
                 self.lastStartAction = 5
 
+            if (
+                FileSystemUtilities.isRemoteFileName(fn)
+                and not self.ui.isEricServerConnected()
+            ):
+                self.__showNotConnectedWarning(title=cap)
+                return
+
             # save the filename for use by the restart method
             self.lastDebuggedFile = fn
             self.restartAct.setEnabled(True)
@@ -2243,6 +2250,13 @@
                     self.clientType = editor.determineFileType()
                 self.lastStartAction = 7
 
+            if (
+                FileSystemUtilities.isRemoteFileName(fn)
+                and not self.ui.isEricServerConnected()
+            ):
+                self.__showNotConnectedWarning(title=cap)
+                return
+
             # save the filename for use by the restart method
             self.lastDebuggedFile = fn
             self.restartAct.setEnabled(True)
@@ -2433,6 +2447,13 @@
                     self.clientType = editor.determineFileType()
                 self.lastStartAction = 3
 
+            if (
+                FileSystemUtilities.isRemoteFileName(fn)
+                and not self.ui.isEricServerConnected()
+            ):
+                self.__showNotConnectedWarning(title=cap)
+                return
+
             # save the filename for use by the restart method
             self.lastDebuggedFile = fn
             self.restartAct.setEnabled(True)
@@ -2515,8 +2536,8 @@
         @param debugProject flag indicating debugging the current project
             (True) or script (False)
         @type bool
-        @param script name of a script (optional)
-        @type str
+        @param script name of a script (defaults to "")
+        @type str (optional)
         """
         from .StartDialog import StartDialog, StartDialogMode
 
@@ -2636,6 +2657,13 @@
                     self.clientType = editor.determineFileType()
                 self.lastStartAction = 1
 
+            if (
+                FileSystemUtilities.isRemoteFileName(fn)
+                and not self.ui.isEricServerConnected()
+            ):
+                self.__showNotConnectedWarning(title=cap)
+                return
+
             # save the filename for use by the restart method
             self.lastDebuggedFile = fn
             self.restartAct.setEnabled(True)
@@ -2828,6 +2856,13 @@
         self.viewmanager.unhighlight()
 
         if not doNotStart:
+            if (
+                FileSystemUtilities.isRemoteFileName(fn)
+                and not self.ui.isEricServerConnected()
+            ):
+                self.__showNotConnectedWarning(title=self.tr("Restart"))
+                return
+
             if forProject and self.project.getProjectType() in ["E7Plugin"]:
                 argv = '--plugin="{0}" {1}'.format(fn, argv)
                 fn = ""  # script name of the eric IDE is set in debug client
@@ -3169,3 +3204,19 @@
         @rtype str
         """
         return self.debugServer.getProjectEnvironmentString()
+
+    def __showNotConnectedWarning(self, title):
+        """
+        Private method to show a warning about a not connected eric-ide server.
+
+        @param title title for the dialog
+        @type str
+        """
+        EricMessageBox.warning(
+            None,
+            title,
+            self.tr(
+                "<p>The selected file is located on an eric-ide server but no such"
+                " server is connected. Aborting...</p>"
+            ),
+        )
--- a/src/eric7/Debugger/DebuggerInterfacePython.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Debugger/DebuggerInterfacePython.py	Fri Feb 23 10:46:46 2024 +0100
@@ -15,7 +15,7 @@
 import struct
 import zlib
 
-from PyQt6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer
+from PyQt6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer, pyqtSlot
 
 from eric7 import Preferences, Utilities
 from eric7.EricWidgets import EricMessageBox
@@ -50,6 +50,23 @@
         self.__autoContinued = []
         self.__isStepCommand = False
 
+        self.__ericServerDebugging = False  # are we debugging via the eric-ide server?
+        try:
+            self.__ericServerDebuggerInterface = (
+                ericApp().getObject("EricServer").getServiceInterface("Debugger")
+            )
+            self.__ericServerDebuggerInterface.debugClientResponse.connect(
+                lambda jsonStr: self.handleJsonCommand(jsonStr, None)
+            )
+            self.__ericServerDebuggerInterface.debugClientDisconnected.connect(
+                self.__handleServerDebugClientDisconnected
+            )
+            self.__ericServerDebuggerInterface.lastClientExited.connect(
+                self.__handleServerLastClientExited
+            )
+        except KeyError:
+            self.__ericServerDebuggerInterface = None
+
         self.debugServer = debugServer
         self.passive = passive
         self.process = None
@@ -88,8 +105,8 @@
         @param fn filename to be translated
         @type str
         @param remote2local flag indicating the direction of translation
-            (False = local to remote, True = remote to local [default])
-        @type bool
+            (False = local to remote, True = remote to local) (defaults to True)
+        @type bool (optional)
         @return translated filename
         @rtype str
         """
@@ -102,8 +119,8 @@
         @param fn filename to be translated
         @type str
         @param remote2local flag indicating the direction of translation
-            (False = local to remote, True = remote to local [default])
-        @type bool
+            (False = local to remote, True = remote to local) (defaults to True)
+        @type bool (optional)
         @return translated filename
         @rtype str
         """
@@ -118,7 +135,24 @@
 
         return path
 
-    def __startProcess(self, program, arguments, environment=None, workingDir=None):
+    def __ericServerTranslation(self, fn, remote2local=True):
+        """
+        Private method to perform the eric-ide server path translation.
+
+        @param fn filename to be translated
+        @type str
+        @param remote2local flag indicating the direction of translation
+            (False = local to remote, True = remote to local) (defaults to True)
+        @type bool (optional)
+        @return translated filename
+        @rtype str
+        """
+        if remote2local:
+            return FileSystemUtilities.remoteFileName(fn)
+        else:
+            return FileSystemUtilities.plainFileName(fn)
+
+    def __startProcess(self, program, arguments, environment=None, workingDir=""):
         """
         Private method to start the debugger client process.
 
@@ -154,8 +188,9 @@
         runInConsole,
         venvName,
         originalPathString,
-        workingDir=None,
+        workingDir="",
         configOverride=None,
+        startRemote=None,
     ):
         """
         Public method to start a remote Python interpreter.
@@ -169,41 +204,61 @@
         @type str
         @param originalPathString original PATH environment variable
         @type str
-        @param workingDir directory to start the debugger client in
-        @type str
-        @param configOverride dictionary containing the global config override
-            data
-        @type dict
+        @param workingDir directory to start the debugger client in (defaults to "")
+        @type str (optional)
+        @param configOverride dictionary containing the global config override data
+            (defaults to None)
+        @type dict (optional)
+        @param startRemote flag indicating to start the client via an eric-ide server
+            (defaults to None)
+        @type bool (optional)
         @return client process object, a flag to indicate a network connection
             and the name of the interpreter in case of a local execution
         @rtype tuple of (QProcess, bool, str)
         """
         global origPathEnv
 
-        if not venvName:
-            venvName = Preferences.getDebugger("Python3VirtualEnv")
-        if venvName == self.debugServer.getProjectEnvironmentString():
-            project = ericApp().getObject("Project")
-            venvName = project.getProjectVenv()
-            execPath = project.getProjectExecPath()
-            interpreter = project.getProjectInterpreter()
+        if (
+            startRemote is True
+            or (
+                startRemote is None
+                and (
+                    venvName == self.debugServer.getEricServerEnvironmentString()
+                    or self.__ericServerDebugging
+                )
+            )
+        ) and ericApp().getObject("EricServer").isServerConnected():
+            # TODO change this once server environment definitions are supported
+            startRemote = True
+            venvName = self.debugServer.getEricServerEnvironmentString()
+            interpreter = ""
         else:
-            venvManager = ericApp().getObject("VirtualEnvManager")
-            interpreter = venvManager.getVirtualenvInterpreter(venvName)
-            execPath = venvManager.getVirtualenvExecPath(venvName)
-        if interpreter == "":
-            # use the interpreter used to run eric for identical variants
-            interpreter = PythonUtilities.getPythonExecutable()
-        if interpreter == "":
-            EricMessageBox.critical(
-                None,
-                self.tr("Start Debugger"),
-                self.tr("""<p>No suitable Python3 environment configured.</p>"""),
-            )
-            return None, False, ""
+            if not venvName:
+                venvName = Preferences.getDebugger("Python3VirtualEnv")
+            if venvName == self.debugServer.getProjectEnvironmentString():
+                project = ericApp().getObject("Project")
+                venvName = project.getProjectVenv()
+                execPath = project.getProjectExecPath()
+                interpreter = project.getProjectInterpreter()
+            else:
+                venvManager = ericApp().getObject("VirtualEnvManager")
+                interpreter = venvManager.getVirtualenvInterpreter(venvName)
+                execPath = venvManager.getVirtualenvExecPath(venvName)
+            if interpreter == "":
+                # use the interpreter used to run eric for identical variants
+                interpreter = PythonUtilities.getPythonExecutable()
+            if interpreter == "":
+                EricMessageBox.critical(
+                    None,
+                    self.tr("Start Debugger"),
+                    self.tr("""<p>No suitable Python3 environment configured.</p>"""),
+                )
+                return None, False, ""
 
         self.__inShutdown = False
 
+        self.__ericServerDebugging = False
+
         redirect = (
             str(configOverride["redirect"])
             if configOverride and configOverride["enable"]
@@ -290,6 +345,28 @@
                 )
                 return None, False, ""
 
+        elif startRemote and self.__ericServerDebuggerInterface is not None:
+            # debugging via an eric-ide server
+            self.translate = self.__ericServerTranslation
+            self.__ericServerDebugging = True
+
+            args = []
+            if noencoding:
+                args.append(noencoding)
+            if multiprocessEnabled:
+                args.append(multiprocessEnabled)
+            if callTraceOptimization:
+                args.append(callTraceOptimization)
+            self.__ericServerDebuggerInterface.startClient(
+                interpreter,
+                originalPathString,
+                args,
+                workingDir=workingDir,
+            )
+            self.__startedVenv = venvName
+
+            return None, self.__isNetworked, ""
+
         else:
             # local debugging code below
             debugClient = self.__determineDebugClient()
@@ -398,6 +475,7 @@
         originalPathString,
         workingDir=None,
         configOverride=None,
+        startRemote=False,
     ):
         """
         Public method to start a remote Python interpreter for a project.
@@ -416,10 +494,15 @@
         @param configOverride dictionary containing the global config override
             data
         @type dict
+        @param startRemote flag indicating to start the client via an eric-ide server
+            (defaults to False)
+        @type bool (optional)
         @return client process object, a flag to indicate a network connection
             and the name of the interpreter in case of a local execution
         @rtype tuple of (QProcess, bool, str)
         """
+        # TODO: 'startRemoteForProject()' - implement the 'startRemote' logic like
+        #       for 'startRemote'.
         global origPathEnv
 
         project = ericApp().getObject("Project")
@@ -460,6 +543,8 @@
 
         self.__inShutdown = False
 
+        self.__ericServerDebugging = False
+
         if project.getDebugProperty("REMOTEDEBUGGER"):
             # remote debugging code
             ipaddr = self.debugServer.getHostAddress(False)
@@ -517,6 +602,8 @@
                 # remote shell command is missing
                 return None, self.__isNetworked, ""
 
+        # TODO: add server debugging for projects
+
         else:
             # local debugging code below
             debugClient = project.getDebugProperty("DEBUGCLIENT")
@@ -619,7 +706,7 @@
         """
         self.__pendingConnections.append(sock)
 
-        sock.readyRead.connect(lambda: self.__parseClientLine(sock))
+        sock.readyRead.connect(lambda: self.__receiveJson(sock))
         sock.disconnected.connect(lambda: self.__socketDisconnected(sock))
 
         return True
@@ -634,30 +721,30 @@
         @param debuggerId id of the connected debug client
         @type str
         """
-        if sock in self.__pendingConnections:
+        if sock and sock in self.__pendingConnections:
             self.__connections[debuggerId] = sock
             self.__pendingConnections.remove(sock)
 
-            if self.__mainDebugger is None:
-                self.__mainDebugger = debuggerId
-                # Get the remote clients capabilities
-                self.remoteCapabilities(debuggerId)
+        if self.__mainDebugger is None:
+            self.__mainDebugger = debuggerId
+            # Get the remote clients capabilities
+            self.remoteCapabilities(debuggerId)
 
-            self.debugServer.signalClientDebuggerId(debuggerId)
+        self.debugServer.signalClientDebuggerId(debuggerId)
 
-            if debuggerId == self.__mainDebugger:
-                self.__flush()
-                self.debugServer.mainClientConnected()
+        if debuggerId == self.__mainDebugger:
+            self.__flush()
+            self.debugServer.mainClientConnected()
 
-            self.debugServer.initializeClient(debuggerId)
+        self.debugServer.initializeClient(debuggerId)
 
-            # perform auto-continue except for main
-            if (
-                debuggerId != self.__mainDebugger
-                and self.__autoContinue
-                and not self.__isStepCommand
-            ):
-                QTimer.singleShot(0, lambda: self.remoteContinue(debuggerId))
+        # perform auto-continue except for main
+        if (
+            debuggerId != self.__mainDebugger
+            and self.__autoContinue
+            and not self.__isStepCommand
+        ):
+            QTimer.singleShot(0, lambda: self.remoteContinue(debuggerId))
 
     def __socketDisconnected(self, sock):
         """
@@ -669,14 +756,7 @@
         for debuggerId in list(self.__connections):
             if self.__connections[debuggerId] is sock:
                 del self.__connections[debuggerId]
-                if debuggerId == self.__mainDebugger:
-                    self.__mainDebugger = None
-                if debuggerId in self.__autoContinued:
-                    self.__autoContinued.remove(debuggerId)
-                if not self.__inShutdown:
-                    with contextlib.suppress(RuntimeError):
-                        # can be ignored during a shutdown
-                        self.debugServer.signalClientDisconnected(debuggerId)
+                self.__handleServerDebugClientDisconnected(debuggerId)
                 break
         else:
             if sock in self.__pendingConnections:
@@ -684,13 +764,37 @@
 
         if not self.__connections:
             # no active connections anymore
+            self.__handleServerLastClientExited()
+
+    @pyqtSlot(str)
+    def __handleServerDebugClientDisconnected(self, debuggerId):
+        """
+        Private slot handling the disconnect of a debug client.
+
+        @param debuggerId ID of the disconnected debugger
+        @type str
+        """
+        if debuggerId == self.__mainDebugger:
+            self.__mainDebugger = None
+        if debuggerId in self.__autoContinued:
+            self.__autoContinued.remove(debuggerId)
+        if not self.__inShutdown:
             with contextlib.suppress(RuntimeError):
-                # debug server object might have been deleted already
-                # ignore this
+                # can be ignored during a shutdown
+                self.debugServer.signalClientDisconnected(debuggerId)
+
+    @pyqtSlot()
+    def __handleServerLastClientExited(self):
+        """
+        Private slot to handle the exit of the last debug client connected.
+        """
+        with contextlib.suppress(RuntimeError):
+            # debug server object might have been deleted already
+            # ignore this
+            self.__autoContinued.clear()
+            if not self.__inShutdown:
                 self.debugServer.signalLastClientExited()
-                self.__autoContinued.clear()
-                if not self.__inShutdown:
-                    self.debugServer.startClient()
+                self.debugServer.startClient()
 
     def getDebuggerIds(self):
         """
@@ -707,9 +811,15 @@
         """
         if self.__mainDebugger:
             # Send commands that were waiting for the connection.
-            conn = self.__connections[self.__mainDebugger]
-            for jsonStr in self.__commandQueue:
-                self.__writeJsonCommandToSocket(jsonStr, conn)
+            if self.__ericServerDebugging:
+                for jsonStr in self.__commandQueue:
+                    self.__ericServerDebuggerInterface.sendClientCommand(
+                        self.__mainDebugger, jsonStr
+                    )
+            else:
+                conn = self.__connections[self.__mainDebugger]
+                for jsonStr in self.__commandQueue:
+                    self.__writeJsonCommandToSocket(jsonStr, conn)
 
         self.__commandQueue.clear()
 
@@ -733,6 +843,8 @@
             sock = self.__pendingConnections.pop()
             self.__shutdownSocket(sock)
 
+        self.__ericServerDebuggerInterface.stopClient()
+
         # reinitialize
         self.__commandQueue.clear()
 
@@ -765,7 +877,7 @@
         @return flag indicating the connection status
         @rtype bool
         """
-        return bool(self.__connections)
+        return bool(self.__connections) or self.__ericServerDebugging
 
     def remoteEnvironment(self, env):
         """
@@ -810,12 +922,15 @@
             instead of unhandled exceptions only
         @type bool
         """
+        if FileSystemUtilities.isPlainFileName(fn):
+            fn = os.path.abspath(fn)
+
         self.__autoContinue = autoContinue
-        self.__scriptName = os.path.abspath(fn)
+        self.__scriptName = fn
         self.__isStepCommand = False
 
         wd = self.translate(wd, False)
-        fn = self.translate(os.path.abspath(fn), False)
+        fn = self.translate(fn, False)
         self.__sendJsonCommand(
             "RequestLoad",
             {
@@ -840,10 +955,13 @@
         @param wd working directory for the program
         @type str
         """
-        self.__scriptName = os.path.abspath(fn)
+        if FileSystemUtilities.isPlainFileName(fn):
+            fn = os.path.abspath(fn)
+
+        self.__scriptName = fn
 
         wd = self.translate(wd, False)
-        fn = self.translate(os.path.abspath(fn), False)
+        fn = self.translate(fn, False)
         self.__sendJsonCommand(
             "RequestRun",
             {
@@ -868,10 +986,13 @@
             cleared first
         @type bool
         """
-        self.__scriptName = os.path.abspath(fn)
+        if FileSystemUtilities.isPlainFileName(fn):
+            fn = os.path.abspath(fn)
+
+        self.__scriptName = fn
 
         wd = self.translate(wd, False)
-        fn = self.translate(os.path.abspath(fn), False)
+        fn = self.translate(fn, False)
         self.__sendJsonCommand(
             "RequestCoverage",
             {
@@ -897,10 +1018,13 @@
             first
         @type bool
         """
-        self.__scriptName = os.path.abspath(fn)
+        if FileSystemUtilities.isPlainFileName(fn):
+            fn = os.path.abspath(fn)
+
+        self.__scriptName = fn
 
         wd = self.translate(wd, False)
-        fn = self.translate(os.path.abspath(fn), False)
+        fn = self.translate(fn, False)
         self.__sendJsonCommand(
             "RequestProfile",
             {
@@ -1042,7 +1166,12 @@
         @param temp flag indicating a temporary breakpoint
         @type bool
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             self.__sendJsonCommand(
                 "RequestBreakpoint",
@@ -1069,7 +1198,12 @@
         @param enable flag indicating enabling or disabling a breakpoint
         @type bool
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             self.__sendJsonCommand(
                 "RequestBreakpointEnable",
@@ -1094,7 +1228,12 @@
         @param count number of occurrences to ignore
         @type int
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             self.__sendJsonCommand(
                 "RequestBreakpointIgnore",
@@ -1119,7 +1258,12 @@
         @param temp flag indicating a temporary watch expression
         @type bool
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             # cond is combination of cond and special (s. watch expression
             # viewer)
@@ -1144,7 +1288,12 @@
         @param enable flag indicating enabling or disabling a watch expression
         @type bool
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             # cond is combination of cond and special (s. watch expression
             # viewer)
@@ -1169,7 +1318,12 @@
         @param count number of occurrences to ignore
         @type int
         """
-        debuggerList = [debuggerId] if debuggerId else list(self.__connections)
+        if debuggerId:
+            debuggerList = [debuggerId]
+        elif self.__ericServerDebugging:
+            debuggerList = ["<<all>>"]
+        else:
+            debuggerList = list(self.__connections)
         for debuggerId in debuggerList:
             # cond is combination of cond and special (s. watch expression
             # viewer)
@@ -1396,7 +1550,7 @@
             debuggerId,
         )
 
-    def __parseClientLine(self, sock):
+    def __receiveJson(self, sock):
         """
         Private method to handle data from the client.
 
@@ -1423,11 +1577,11 @@
             logging.debug("<Debug-Server> %s", jsonStr)
             ##print("Server: ", jsonStr)    ## debug       # __IGNORE_WARNING_M891__
 
-            self.__handleJsonCommand(jsonStr, sock)
+            self.handleJsonCommand(jsonStr, sock)
 
-    def __handleJsonCommand(self, jsonStr, sock):
+    def handleJsonCommand(self, jsonStr, sock):
         """
-        Private method to handle a command or response serialized as a
+        Public method to handle a command or response serialized as a
         JSON string.
 
         @param jsonStr string containing the command or response received
@@ -1553,6 +1707,7 @@
             self.debugServer.signalClientRawInput(
                 params["prompt"], params["echo"], params["debuggerId"]
             )
+            pass
 
         elif method == "ResponseBPConditionError":
             fn = self.translate(params["filename"], True)
@@ -1622,7 +1777,7 @@
         elif method == "ResponseExit":
             self.__scriptName = ""
             self.debugServer.signalClientExit(
-                params["program"],
+                self.translate(params["program"], True),
                 params["status"],
                 params["message"],
                 params["debuggerId"],
@@ -1663,14 +1818,26 @@
         }
         jsonStr = json.dumps(commandDict)
 
-        if debuggerId and debuggerId in self.__connections:
-            sock = self.__connections[debuggerId]
-        elif sock is None and self.__mainDebugger is not None:
-            sock = self.__connections[self.__mainDebugger]
-        if sock is not None:
-            self.__writeJsonCommandToSocket(jsonStr, sock)
+        if self.__ericServerDebugging:
+            # Debugging via the eric-ide server -> pass the command on to it
+            if self.__mainDebugger is None:
+                # debugger has not connected yet -> queue the command
+                self.__commandQueue.append(jsonStr)
+            else:
+                self.__ericServerDebuggerInterface.sendClientCommand(
+                    debuggerId, jsonStr
+                )
         else:
-            self.__commandQueue.append(jsonStr)
+            # Local debugging -> send the command to the client
+            if debuggerId and debuggerId in self.__connections:
+                sock = self.__connections[debuggerId]
+            elif sock is None and self.__mainDebugger is not None:
+                with contextlib.suppress(KeyError):
+                    sock = self.__connections[self.__mainDebugger]
+            if sock is not None:
+                self.__writeJsonCommandToSocket(jsonStr, sock)
+            else:
+                self.__commandQueue.append(jsonStr)
 
     def __writeJsonCommandToSocket(self, jsonCommand, sock):
         """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/EricGui/EricFileIconProvider.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,208 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a file icon provider determining the icon based on file name.
+"""
+
+import fnmatch
+
+from PyQt6.QtGui import QImageReader
+
+from eric7.EricGui import EricPixmapCache
+
+
+class EricFileIconProvider:
+    """
+    Class implementing a file icon provider determining the icon based on file name.
+    """
+
+    def __init__(self):
+        """
+        Constructor
+        """
+        # pixmap icon names first because some are overwritten later
+        self.__iconMappings = {
+            "*.{0}".format(bytes(f).decode()): "filePixmap"
+            for f in QImageReader.supportedImageFormats()
+        }
+
+        # specific one next
+        self.__iconMappings.update(
+            {
+                "*.sh": "lexerBash",
+                "*.bash": "lexerBash",
+                "*.bat": "lexerBatch",
+                "*.cmd": "lexerBatch",
+                "*.cpp": "lexerCPP",
+                "*.cxx": "lexerCPP",
+                "*.cc": "lexerCPP",
+                "*.c": "lexerCPP",
+                "*.hpp": "lexerCPP",
+                "*.hh": "lexerCPP",
+                "*.h": "lexerCPP",
+                "*.cs": "lexerCsharp",
+                "CMakeLists.txt": "lexerCMake",
+                "*.cmake": "lexerCMake",
+                "*.cmake.in": "lexerCMake",
+                "*.ctest": "lexerCMake",
+                "*.ctest.in": "lexerCMake",
+                "*.css": "lexerCSS",
+                "*.qss": "lexerCSS",
+                "*.d": "lexerD",
+                "*.di": "lexerD",
+                "*.diff": "lexerDiff",
+                "*.patch": "lexerDiff",
+                "*.html": "lexerHTML",
+                "*.htm": "lexerHTML",
+                "*.asp": "lexerHTML",
+                "*.shtml": "lexerHTML",
+                "*.php": "lexerHTML",
+                "*.php3": "lexerHTML",
+                "*.php4": "lexerHTML",
+                "*.php5": "lexerHTML",
+                "*.phtml": "lexerHTML",
+                "*.docbook": "lexerHTML",
+                "*.ui": "fileDesigner",
+                "*.ts": "fileLinguist",
+                "*.qm": "fileLinguist2",
+                "*.qrc": "fileResource",
+                "*.kid": "lexerHTML",
+                "*.java": "lexerJava",
+                "*.js": "lexerJavaScript",
+                "*.lua": "lexerLua",
+                "*makefile": "lexerMakefile",
+                "Makefile*": "lexerMakefile",
+                "*.mak": "lexerMakefile",
+                "*.pl": "lexerPerl",
+                "*.pm": "lexerPerl",
+                "*.ph": "lexerPerl",
+                "*.pov": "lexerPovray",
+                "*.properties": "lexerProperties",
+                "*.ini": "lexerProperties",
+                "*.inf": "lexerProperties",
+                "*.reg": "lexerProperties",
+                "*.cfg": "lexerProperties",
+                "*.cnf": "lexerProperties",
+                "*.rc": "lexerProperties",
+                "*.py": "lexerPython3",
+                "*.pyw": "lexerPython3",
+                "*.py3": "lexerPython3",
+                "*.pyw3": "lexerPython3",
+                "*.pyx": "lexerCython",
+                "*.pxd": "lexerCython",
+                "*.pxi": "lexerCython",
+                "*.ptl": "lexerPython3",
+                "*.rb": "lexerRuby",
+                "*.rbw": "lexerRuby",
+                "*.sql": "lexerSQL",
+                "*.tex": "lexerTeX",
+                "*.sty": "lexerTeX",
+                "*.aux": "lexerTeX",
+                "*.toc": "lexerTeX",
+                "*.idx": "lexerTeX",
+                "*.vhd": "lexerVHDL",
+                "*.vhdl": "lexerVHDL",
+                "*.tcl": "lexerTCL",
+                "*.tk": "lexerTCL",
+                "*.f": "lexerFortran",
+                "*.for": "lexerFortran",
+                "*.f90": "lexerFortran",
+                "*.f95": "lexerFortran",
+                "*.f2k": "lexerFortran",
+                "*.dpr": "lexerPascal",
+                "*.dpk": "lexerPascal",
+                "*.pas": "lexerPascal",
+                "*.dfm": "lexerPascal",
+                "*.inc": "lexerPascal",
+                "*.pp": "lexerPascal",
+                "*.ps": "lexerPostScript",
+                "*.xml": "lexerXML",
+                "*.xsl": "lexerXML",
+                "*.svg": "fileSvg",
+                "*.xsd": "lexerXML",
+                "*.xslt": "lexerXML",
+                "*.dtd": "lexerXML",
+                "*.rdf": "lexerXML",
+                "*.xul": "lexerXML",
+                "*.yaml": "lexerYAML",
+                "*.yml": "lexerYAML",
+                "*.m": "lexerMatlab",
+                "*.m.matlab": "lexerMatlab",
+                "*.m.octave": "lexerOctave",
+                "*.e4c": "lexerXML",
+                "*.e4d": "lexerXML",
+                "*.e4k": "fileShortcuts",
+                "*.e4m": "fileMultiProject",
+                "*.e4p": "fileProject",
+                "*.e4q": "lexerXML",
+                "*.e4s": "lexerXML",
+                "*.e4t": "lexerXML",
+                "*.e5d": "lexerXML",
+                "*.e5g": "fileUML",
+                "*.e5k": "fileShortcuts",
+                "*.e5m": "fileMultiProject",
+                "*.e5p": "fileProject",
+                "*.e5q": "lexerXML",
+                "*.e5s": "lexerXML",
+                "*.e5t": "lexerXML",
+                "*.e6d": "lexerXML",
+                "*.e6k": "fileShortcuts",
+                "*.e6m": "fileMultiProject",
+                "*.e6p": "fileProject",
+                "*.e6q": "lexerXML",
+                "*.e6s": "lexerXML",
+                "*.e6t": "lexerXML",
+                "*.ecj": "lexerJSON",
+                "*.edj": "lexerJSON",
+                "*.egj": "fileUML",
+                "*.ehj": "lexerJSON",
+                "*.ekj": "fileShortcuts",
+                "*.emj": "fileMultiProject",
+                "*.epj": "fileProject",
+                "*.eqj": "lexerJSON",
+                "*.esj": "lexerJSON",
+                "*.etj": "lexerJSON",
+                "*.ethj": "lexerJSON",
+                "*.po": "lexerGettext",
+                "*.coffee": "lexerCoffeeScript",
+                "*.json": "lexerJSON",
+                "*.md": "lexerMarkdown",
+                "*.toml": "lexerProperties",
+                "Pipfile": "lexerProperties",
+                "poetry.lock": "lexerProperties",
+                "*.pdf": "pdfviewer",
+            }
+        )
+
+    def fileIcon(self, name):
+        """
+        Public method to get an icon for the given file name.
+
+        @param name file name
+        @type str
+        @return icon
+        @rtype QIcon
+        """
+        for pat in self.__iconMappings:
+            if fnmatch.fnmatch(name, pat):
+                return EricPixmapCache.getIcon(self.__iconMappings[pat])
+        else:
+            return EricPixmapCache.getIcon("fileMisc")
+
+    def fileIconName(self, name):
+        """
+        Public method to get an icon name for the given file name.
+
+        @param name file name
+        @type str
+        @return icon name
+        @rtype str
+        """
+        for pat in self.__iconMappings:
+            if fnmatch.fnmatch(name, pat):
+                return self.__iconMappings[pat]
+        else:
+            return "fileMisc"
--- a/src/eric7/Globals/__init__.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Globals/__init__.py	Fri Feb 23 10:46:46 2024 +0100
@@ -197,7 +197,7 @@
     """
     if loc is None:
         if size < 1024:
-            return QCoreApplication.translate("Globals", "{0:4.2f} Bytes").format(size)
+            return QCoreApplication.translate("Globals", "{0:4d} Bytes").format(size)
         elif size < 1024 * 1024:
             size /= 1024
             return QCoreApplication.translate("Globals", "{0:4.2f} KiB").format(size)
@@ -211,9 +211,12 @@
             size /= 1024 * 1024 * 1024 * 1024
             return QCoreApplication.translate("Globals", "{0:4.2f} TiB").format(size)
     else:
+        if not isinstance(size, float):
+            size = float(size)
+
         if size < 1024:
             return QCoreApplication.translate("Globals", "{0} Bytes").format(
-                loc.toString(size, "f", 2)
+                loc.toString(size)
             )
         elif size < 1024 * 1024:
             size /= 1024
--- a/src/eric7/Graphics/ApplicationDiagramBuilder.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Graphics/ApplicationDiagramBuilder.py	Fri Feb 23 10:46:46 2024 +0100
@@ -15,6 +15,7 @@
 
 from eric7 import Globals, Preferences
 from eric7.EricWidgets import EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
 from eric7.EricWidgets.EricProgressDialog import EricProgressDialog
 from eric7.SystemUtilities import FileSystemUtilities
 
@@ -49,6 +50,10 @@
             self.tr("Application Diagram {0}").format(self.project.getProjectName())
         )
 
+        self.__remotefsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
     def __buildModulesDict(self):
         """
         Private method to build a dictionary of modules contained in the
@@ -64,9 +69,16 @@
         mods = self.project.getProjectData(dataKey="SOURCES")
         modules = []
         for module in mods:
-            modules.append(
-                FileSystemUtilities.normabsjoinpath(self.project.ppath, module)
-            )
+            if FileSystemUtilities.isRemoteFileName(self.project.getProjectPath()):
+                modules.append(
+                    self.__remotefsInterface.join(self.project.getProjectPath(), module)
+                )
+            else:
+                modules.append(
+                    FileSystemUtilities.normabsjoinpath(
+                        self.project.getProjectPath(), module
+                    )
+                )
         tot = len(modules)
         progress = EricProgressDialog(
             self.tr("Parsing modules..."),
@@ -111,26 +123,48 @@
         @rtype str
         """
         candidates = []
-        path = self.project.getProjectPath()
-        init = os.path.join(path, "__init__.py")
-        if os.path.exists(init):
-            # project is a package
-            return path
+        ppath = self.project.getProjectPath()
+
+        if FileSystemUtilities.isRemoteFileName(ppath):
+            init = self.__remotefsInterface.join(ppath, "__init__.py")
+            if self.__remotefsInterface.exists(init):
+                # remote project is a package
+                return ppath
+            else:
+                # check, if any of the top directories is a package
+                for entry in self.__remotefsInterface.listdir(ppath)[2]:
+                    if (
+                        not entry["name"].startswith(".")
+                        and entry["is_dir"]
+                        and self.__remotefsInterface.exists(
+                            self.__remotefsInterface.join(entry["path"], "__init__.py")
+                        )
+                    ):
+                        candidates.append(entry["path"])
+
+                # check, if project uses the 'src' layout
+                srcPath = self.__remotefsInterface.join(ppath, "src")
+                if self.__remotefsInterface.exists(srcPath):
+                    for entry in self.__remotefsInterface.listdir(srcPath)[2]:
+                        if (
+                            not entry["name"].startswith(".")
+                            and entry["is_dir"]
+                            and self.__remotefsInterface.exists(
+                                self.__remotefsInterface.join(
+                                    entry["path"], "__init__.py"
+                                )
+                            )
+                        ):
+                            candidates.append(entry["path"])
+
         else:
-            # check, if any of the top directories is a package
-            with os.scandir(path) as dirEntriesIterator:
-                for entry in [
-                    e for e in dirEntriesIterator if not e.name.startswith(".")
-                ]:
-                    if entry.is_dir() and os.path.exists(
-                        os.path.join(entry.path, "__init__.py")
-                    ):
-                        candidates.append(entry.path)
-
-            # check, if project uses the 'src' layout
-            srcPath = os.path.join(path, "src")
-            if os.path.exists(srcPath):
-                with os.scandir(srcPath) as dirEntriesIterator:
+            init = os.path.join(ppath, "__init__.py")
+            if os.path.exists(init):
+                # project is a package
+                return ppath
+            else:
+                # check, if any of the top directories is a package
+                with os.scandir(ppath) as dirEntriesIterator:
                     for entry in [
                         e for e in dirEntriesIterator if not e.name.startswith(".")
                     ]:
@@ -139,6 +173,18 @@
                         ):
                             candidates.append(entry.path)
 
+                # check, if project uses the 'src' layout
+                srcPath = os.path.join(ppath, "src")
+                if os.path.exists(srcPath):
+                    with os.scandir(srcPath) as dirEntriesIterator:
+                        for entry in [
+                            e for e in dirEntriesIterator if not e.name.startswith(".")
+                        ]:
+                            if entry.is_dir() and os.path.exists(
+                                os.path.join(entry.path, "__init__.py")
+                            ):
+                                candidates.append(entry.path)
+
             if len(candidates) == 1:
                 return candidates[0]
             elif len(candidates) > 1:
@@ -172,7 +218,13 @@
             # no root path detected
             return
 
-        root = os.path.splitdrive(rpath)[1].replace(os.sep, ".")[1:]
+        root = (
+            self.__remotefsInterface.splitdrive(rpath)[1][1:].replace(
+                self.__remotefsInterface.separator(), "."
+            )
+            if FileSystemUtilities.isRemoteFileName(rpath)
+            else os.path.splitdrive(rpath)[1][1:].replace(os.sep, ".")
+        )
 
         packages = {}
         self.__shapes = {}
--- a/src/eric7/Graphics/ImportsDiagramBuilder.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Graphics/ImportsDiagramBuilder.py	Fri Feb 23 10:46:46 2024 +0100
@@ -14,6 +14,7 @@
 from PyQt6.QtWidgets import QApplication, QGraphicsTextItem
 
 from eric7 import Globals, Preferences
+from eric7.EricWidgets.EricApplication import ericApp
 from eric7.EricWidgets.EricProgressDialog import EricProgressDialog
 from eric7.SystemUtilities import FileSystemUtilities
 
@@ -49,7 +50,10 @@
         self.setObjectName("ImportsDiagram")
 
         self.showExternalImports = showExternalImports
-        self.packagePath = os.path.abspath(package)
+        if FileSystemUtilities.isRemoteFileName(package):
+            self.packagePath = package
+        else:
+            self.packagePath = os.path.abspath(package)
 
         self.__relPackagePath = (
             self.project.getRelativePath(self.packagePath)
@@ -57,16 +61,33 @@
             else ""
         )
 
+        self.__remotefsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
     def initialize(self):
         """
         Public method to initialize the object.
         """
-        self.package = os.path.splitdrive(self.packagePath)[1].replace(os.sep, ".")[1:]
         hasInit = True
         ppath = self.packagePath
-        while hasInit:
-            ppath = os.path.dirname(ppath)
-            hasInit = len(glob.glob(os.path.join(ppath, "__init__.*"))) > 0
+
+        if FileSystemUtilities.isRemoteFileName(self.packagePath):
+            self.package = self.__remotefsInterface.splitdrive(self.packagePath)[1][
+                1:
+            ].replace(self.__remotefsInterface.separator(), ".")
+            while hasInit:
+                ppath = self.__remotefsInterface.dirname(ppath)
+                globPattern = self.__remotefsInterface.join(ppath, "__init__.*")
+                hasInit = len(self.__remotefsInterface.glob(globPattern)) > 0
+        else:
+            self.package = os.path.splitdrive(self.packagePath)[1][1:].replace(
+                os.sep, "."
+            )
+            while hasInit:
+                ppath = os.path.dirname(ppath)
+                hasInit = len(glob.glob(os.path.join(ppath, "__init__.*"))) > 0
+
         self.shortPackage = self.packagePath.replace(ppath, "").replace(os.sep, ".")[1:]
 
         pname = self.project.getProjectName()
@@ -93,13 +114,21 @@
         moduleDict = {}
         modules = []
         for ext in Preferences.getPython("Python3Extensions"):
-            modules.extend(
-                glob.glob(
-                    FileSystemUtilities.normjoinpath(
-                        self.packagePath, "*{0}".format(ext)
+            if FileSystemUtilities.isRemoteFileName(self.packagePath):
+                modules.extend(
+                    [
+                        FileSystemUtilities.remoteFileName(f)
+                        for f in self.__remotefsInterface.glob(
+                            self.__remotefsInterface.join(self.packagePath, f"*{ext}")
+                        )
+                    ]
+                )
+            else:
+                modules.extend(
+                    glob.glob(
+                        FileSystemUtilities.normjoinpath(self.packagePath, f"*{ext}")
                     )
                 )
-            )
 
         tot = len(modules)
         progress = EricProgressDialog(
@@ -141,7 +170,11 @@
         """
         Public method to build the modules shapes of the diagram.
         """
-        initlist = glob.glob(os.path.join(self.packagePath, "__init__.*"))
+        if FileSystemUtilities.isRemoteFileName(self.packagePath):
+            globPattern = self.__remotefsInterface.join(self.packagePath, "__init__.*")
+            initlist = self.__remotefsInterface.glob(globPattern)
+        else:
+            initlist = glob.glob(os.path.join(self.packagePath, "__init__.*"))
         if len(initlist) == 0:
             ct = QGraphicsTextItem(None)
             ct.setHtml(
--- a/src/eric7/Graphics/PackageDiagramBuilder.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Graphics/PackageDiagramBuilder.py	Fri Feb 23 10:46:46 2024 +0100
@@ -16,6 +16,7 @@
 from PyQt6.QtWidgets import QApplication, QGraphicsTextItem
 
 from eric7 import Globals, Preferences
+from eric7.EricWidgets.EricApplication import ericApp
 from eric7.EricWidgets.EricProgressDialog import EricProgressDialog
 from eric7.SystemUtilities import FileSystemUtilities
 
@@ -45,7 +46,10 @@
         super().__init__(dialog, view, project)
         self.setObjectName("PackageDiagram")
 
-        self.package = os.path.abspath(package)
+        if FileSystemUtilities.isRemoteFileName(package):
+            self.package = package
+        else:
+            self.package = os.path.abspath(package)
         self.noAttrs = noAttrs
 
         self.__relPackage = (
@@ -54,6 +58,10 @@
             else ""
         )
 
+        self.__remotefsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
     def initialize(self):
         """
         Public method to initialize the object.
@@ -97,9 +105,19 @@
         moduleDict = {}
         modules = []
         for ext in supportedExt:
-            modules.extend(
-                glob.glob(FileSystemUtilities.normjoinpath(self.package, ext))
-            )
+            if FileSystemUtilities.isRemoteFileName(self.package):
+                modules.extend(
+                    [
+                        FileSystemUtilities.remoteFileName(f)
+                        for f in self.__remotefsInterface.glob(
+                            self.__remotefsInterface.join(self.package, ext)
+                        )
+                    ]
+                )
+            else:
+                modules.extend(
+                    glob.glob(FileSystemUtilities.normjoinpath(self.package, ext))
+                )
         tot = len(modules)
         progress = EricProgressDialog(
             self.tr("Parsing modules..."),
@@ -154,19 +172,47 @@
         subpackagesDict = {}
         subpackagesList = []
 
-        with os.scandir(self.package) as dirEntriesIterator:
-            for subpackage in dirEntriesIterator:
+        if FileSystemUtilities.isRemoteFileName(self.package):
+            for subpackage in self.__remotefsInterface.listdir(self.package)[2]:
                 if (
-                    subpackage.is_dir()
-                    and subpackage.name != "__pycache__"
-                    and len(glob.glob(os.path.join(subpackage.path, "__init__.*"))) != 0
+                    subpackage["is_dir"]
+                    and subpackage["name"] != "__pycache__"
+                    and len(
+                        self.__remotefsInterface.glob(
+                            self.__remotefsInterface.join(
+                                subpackage["path"], "__init__.*"
+                            )
+                        )
+                    )
+                    != 0
                 ):
-                    subpackagesList.append(subpackage.path)
+                    subpackagesList.append(
+                        FileSystemUtilities.remoteFileName(subpackage["path"])
+                    )
+        else:
+            with os.scandir(self.package) as dirEntriesIterator:
+                for subpackage in dirEntriesIterator:
+                    if (
+                        subpackage.is_dir()
+                        and subpackage.name != "__pycache__"
+                        and len(glob.glob(os.path.join(subpackage.path, "__init__.*")))
+                        != 0
+                    ):
+                        subpackagesList.append(subpackage.path)
 
         tot = 0
         for ext in supportedExt:
             for subpackage in subpackagesList:
-                tot += len(glob.glob(FileSystemUtilities.normjoinpath(subpackage, ext)))
+                if FileSystemUtilities.isRemoteFileName(subpackage):
+                    tot += len(
+                        self.__remotefsInterface.glob(
+                            self.__remotefsInterface.join(subpackage, ext)
+                        )
+                    )
+                else:
+                    tot += len(
+                        glob.glob(FileSystemUtilities.normjoinpath(subpackage, ext))
+                    )
         progress = EricProgressDialog(
             self.tr("Parsing modules..."),
             None,
@@ -187,9 +233,19 @@
                 subpackagesDict[packageName] = []
                 modules = []
                 for ext in supportedExt:
-                    modules.extend(
-                        glob.glob(FileSystemUtilities.normjoinpath(subpackage, ext))
-                    )
+                    if FileSystemUtilities.isRemoteFileName(subpackage):
+                        modules.extend(
+                            [
+                                FileSystemUtilities.remoteFileName(f)
+                                for f in self.__remotefsInterface.glob(
+                                    self.__remotefsInterface.join(subpackage, ext)
+                                )
+                            ]
+                        )
+                    else:
+                        modules.extend(
+                            glob.glob(FileSystemUtilities.normjoinpath(subpackage, ext))
+                        )
                 for prog, module in enumerate(modules, start=start):
                     progress.setValue(prog)
                     if time.monotonic() - now > 0.01:
@@ -225,7 +281,12 @@
         """
         self.allClasses = {}
 
-        initlist = glob.glob(os.path.join(self.package, "__init__.*"))
+        globPattern = os.path.join(self.package, "__init__.*")
+        initlist = (
+            self.__remotefsInterface.glob(globPattern)
+            if FileSystemUtilities.isRemoteFileName(self.package)
+            else glob.glob(globPattern)
+        )
         if len(initlist) == 0:
             ct = QGraphicsTextItem(None)
             self.scene.addItem(ct)
--- a/src/eric7/JediInterface/AssistantJedi.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/JediInterface/AssistantJedi.py	Fri Feb 23 10:46:46 2024 +0100
@@ -13,6 +13,7 @@
 from PyQt6.QtWidgets import QMenu
 
 from eric7 import Preferences
+from eric7.SystemUtilities import FileSystemUtilities
 
 from .JediServer import JediServer
 
@@ -254,4 +255,7 @@
         @type Editor
         """
         if menuName == "Main":
-            self.__menu.setEnabled(editor.hasSelectedText())
+            self.__menu.setEnabled(
+                not FileSystemUtilities.isRemoteFileName(editor.getFileName())
+                and editor.hasSelectedText()
+            )
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py	Fri Feb 23 10:46:46 2024 +0100
@@ -202,6 +202,10 @@
         self.mainWidget.setCurrentWidget(self.configureTab)
         self.optionsTabWidget.setCurrentWidget(self.globalOptionsTab)
 
+        self.__remotefsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
     def __defaultStatistics(self):
         """
         Private method to return the default statistics entry.
@@ -840,13 +844,21 @@
 
         if isinstance(fn, list):
             self.files = fn[:]
-        elif os.path.isdir(fn):
-            self.files = []
+        elif FileSystemUtilities.isRemoteFileName(
+            fn
+        ) and self.__remotefsInterface.isdir(fn):
             extensions = set(Preferences.getPython("Python3Extensions"))
-            for ext in extensions:
-                self.files.extend(
-                    FileSystemUtilities.direntries(fn, True, "*{0}".format(ext), 0)
+            self.files = [
+                FileSystemUtilities.remoteFileName(f)
+                for f in self.__remotefsInterface.direntries(
+                    fn, True, [f"*{ext}" for ext in extensions], False
                 )
+            ]
+        elif FileSystemUtilities.isPlainFileName(fn) and os.path.isdir(fn):
+            extensions = set(Preferences.getPython("Python3Extensions"))
+            self.files = FileSystemUtilities.direntries(
+                fn, True, [f"*{ext}" for ext in extensions], False
+            )
         else:
             self.files = [fn]
 
@@ -1085,7 +1097,12 @@
             encoding = Utilities.get_coding(source)
         else:
             try:
-                source, encoding = Utilities.readEncodedFile(self.filename)
+                if FileSystemUtilities.isRemoteFileName(self.filename):
+                    source, encoding = self.__remotefsInterface.readEncodedFile(
+                        self.filename
+                    )
+                else:
+                    source, encoding = Utilities.readEncodedFile(self.filename)
                 source = source.splitlines(True)
             except (OSError, UnicodeError) as msg:
                 self.results = CodeStyleCheckerDialog.hasResults
@@ -1131,7 +1148,12 @@
                 self.__timenow = time.monotonic()
 
             try:
-                source, encoding = Utilities.readEncodedFile(filename)
+                if FileSystemUtilities.isRemoteFileName(filename):
+                    source, encoding = self.__remotefsInterface.readEncodedFile(
+                        filename
+                    )
+                else:
+                    source, encoding = Utilities.readEncodedFile(filename)
                 source = source.splitlines(True)
             except (OSError, UnicodeError) as msg:
                 self.results = CodeStyleCheckerDialog.hasResults
--- a/src/eric7/Plugins/CheckerPlugins/SyntaxChecker/SyntaxCheckerDialog.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Plugins/CheckerPlugins/SyntaxChecker/SyntaxCheckerDialog.py	Fri Feb 23 10:46:46 2024 +0100
@@ -87,6 +87,10 @@
             self.syntaxCheckService = None
         self.filename = None
 
+        self.__remotefsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
     def __resort(self):
         """
         Private method to resort the tree.
@@ -421,7 +425,12 @@
             self.source = Utilities.normalizeCode(codestring)
         else:
             try:
-                self.source = Utilities.readEncodedFile(self.filename)[0]
+                if FileSystemUtilities.isRemoteFileName(self.filename):
+                    self.source = self.__remotefsInterface.readEncodedFile(
+                        self.filename
+                    )[0]
+                else:
+                    self.source = Utilities.readEncodedFile(self.filename)[0]
                 self.source = Utilities.normalizeCode(self.source)
             except (OSError, UnicodeError) as msg:
                 self.noResults = False
@@ -458,7 +467,11 @@
                 self.__timenow = time.monotonic()
 
             try:
-                source = Utilities.readEncodedFile(filename)[0]
+                source = (
+                    self.__remotefsInterface.readEncodedFile(self.filename)[0]
+                    if FileSystemUtilities.isRemoteFileName(self.filename)
+                    else Utilities.readEncodedFile(filename)[0]
+                )
                 source = Utilities.normalizeCode(source)
             except (OSError, UnicodeError) as msg:
                 self.noResults = False
--- a/src/eric7/Plugins/PluginCodeStyleChecker.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Plugins/PluginCodeStyleChecker.py	Fri Feb 23 10:46:46 2024 +0100
@@ -476,6 +476,7 @@
         if menuName == "Checks":
             if self.__editorAct not in menu.actions():
                 menu.addAction(self.__editorAct)
+            # TODO: disable the action for eric-ide server files
             self.__editorAct.setEnabled(editor.isPyFile())
 
     def __editorCodeStyleCheck(self):
--- a/src/eric7/Preferences/ConfigurationDialog.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Preferences/ConfigurationDialog.py	Fri Feb 23 10:46:46 2024 +0100
@@ -210,6 +210,13 @@
                     None,
                     None,
                 ],
+                "ericServerPage": [
+                    self.tr("eric-ide Server"),
+                    "preferences-eric-server",
+                    "EricServerPage",
+                    None,
+                    None,
+                ],
                 "graphicsPage": [
                     self.tr("Graphics"),
                     "preferences-graphics",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Preferences/ConfigurationPages/EricServerPage.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the eric-ide server related settings.
+"""
+
+from eric7 import Preferences
+
+from .ConfigurationPageBase import ConfigurationPageBase
+from .Ui_EricServerPage import Ui_EricServerPage
+
+
+class EricServerPage(ConfigurationPageBase, Ui_EricServerPage):
+    """
+    Class implementing the eric-ide server related settings.
+    """
+
+    def __init__(self):
+        """
+        Constructor
+        """
+        super().__init__()
+        self.setupUi(self)
+        self.setObjectName("EricServerPage")
+
+        # set initial values
+        self.timeoutSpinBox.setValue(Preferences.getEricServer("ConnectionTimeout"))
+        self.startShellCheckBox.setChecked(Preferences.getEricServer("AutostartShell"))
+
+    def save(self):
+        """
+        Public slot to save the Cooperation configuration.
+        """
+        Preferences.setEricServer("ConnectionTimeout", self.timeoutSpinBox.value())
+        Preferences.setEricServer("AutostartShell", self.startShellCheckBox.isChecked())
+
+
+def create(dlg):  # noqa: U100
+    """
+    Module function to create the configuration page.
+
+    @param dlg reference to the configuration dialog
+    @type ConfigurationDialog
+    @return reference to the instantiated page
+    @rtype ConfigurationPageBase
+    """
+    page = EricServerPage()
+    return page
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/Preferences/ConfigurationPages/EricServerPage.ui	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EricServerPage</class>
+ <widget class="QWidget" name="EricServerPage">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>510</width>
+    <height>452</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="QLabel" name="headerLabel">
+     <property name="text">
+      <string>&lt;b&gt;Configure eric-ide Server Settings&lt;/b&gt;</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line11">
+     <property name="frameShape">
+      <enum>QFrame::HLine</enum>
+     </property>
+     <property name="frameShadow">
+      <enum>QFrame::Sunken</enum>
+     </property>
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>&lt;b&gt;Note:&lt;/b&gt; The eric-ide server is configured via command line parameters. The parameters of this page configure the interface to the eric-ide server.</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Server Connection</string>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout">
+      <item>
+       <widget class="QLabel" name="label">
+        <property name="text">
+         <string>Default Timeout:</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QSpinBox" name="timeoutSpinBox">
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+        <property name="suffix">
+         <string> s</string>
+        </property>
+        <property name="minimum">
+         <number>5</number>
+        </property>
+        <property name="maximum">
+         <number>60</number>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>294</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox_2">
+     <property name="title">
+      <string>Shell</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <item>
+       <widget class="QCheckBox" name="startShellCheckBox">
+        <property name="toolTip">
+         <string>Select this to start an eric-ide Server Shell when a connection to an eric-ide Server is established.</string>
+        </property>
+        <property name="text">
+         <string>Start server Shell when server is conncted</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>87</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>timeoutSpinBox</tabstop>
+  <tabstop>startShellCheckBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
--- a/src/eric7/Preferences/__init__.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Preferences/__init__.py	Fri Feb 23 10:46:46 2024 +0100
@@ -1709,7 +1709,7 @@
     else:
         jediDefaults["MouseClickGotoModifiers"] = Qt.KeyboardModifier.ControlModifier
 
-    # defaults for Hex Editor
+    # defaults for PDF viewer
     pdfViewerDefaults = {
         "PdfViewerState": QByteArray(),
         "PdfViewerSplitterState": QByteArray(),
@@ -1723,6 +1723,13 @@
         "PdfSearchHighlightAll": True,
     }
 
+    # defaults for the eric-ide server interface
+    ericServerDefaults = {
+        "ConnectionProfiles": "{}",  # JSON encoded dictionary
+        "ConnectionTimeout": 10,  # timeout in seconds
+        "AutostartShell": True,
+    }
+
 
 def readToolGroups():
     """
@@ -4141,7 +4148,7 @@
 
 def getPdfViewer(key):
     """
-    Module function to retrieve the Pdf Viewer related settings.
+    Module function to retrieve the PDF Viewer related settings.
 
     @param key the key of the value to get
     @type str
@@ -4174,7 +4181,7 @@
 
 def setPdfViewer(key, value):
     """
-    Module function to store the Pdf Viewer related settings.
+    Module function to store the PDF Viewer related settings.
 
     @param key the key of the setting to be set
     @type str
@@ -4186,6 +4193,52 @@
     Prefs.settings.setValue("PdfViewer/" + key, value)
 
 
+def getEricServer(key):
+    """
+    Module function to retrieve the eric-ide server interface related settings.
+
+    @param key the key of the value to get
+    @type str
+    @return the requested user setting
+    @rtype Any
+    """
+    prefix = "EricServer/"
+
+    if key in ("ConnectionTimeout",):
+        return int(
+            Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key])
+        )
+    elif key in ("AutostartShell",):
+        return toBool(
+            Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key])
+        )
+    elif key in ("ConnectionProfiles",):
+        jsonStr = Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key])
+        if jsonStr:
+            return json.loads(jsonStr)
+        else:
+            return None
+    else:
+        return Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key])
+
+
+def setEricServer(key, value):
+    """
+    Module function to store the eric-ide server interface related settings.
+
+    @param key the key of the setting to be set
+    @type str
+    @param value the value to be set
+    @type Any
+    """
+    prefix = "EricServer/"
+
+    if key in ("ConnectionProfiles",):
+        Prefs.settings.setValue(f"{prefix}{key}", json.dumps(value))
+    else:
+        Prefs.settings.setValue(f"{prefix}{key}", value)
+
+
 def getGeometry(key):
     """
     Module function to retrieve the display geometry.
--- a/src/eric7/Project/DebuggerPropertiesFile.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Project/DebuggerPropertiesFile.py	Fri Feb 23 10:46:46 2024 +0100
@@ -17,6 +17,8 @@
 from eric7 import Preferences
 from eric7.EricGui.EricOverrideCursor import EricOverridenCursor
 from eric7.EricWidgets import EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities
 
 Project = typing.TypeVar("Project")
 
@@ -48,6 +50,10 @@
         @return flag indicating a successful write
         @rtype bool
         """
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
         debuggerPropertiesDict = {
             "header": {
                 "comment": "eric debugger properties file for project {0}".format(
@@ -66,13 +72,18 @@
 
         try:
             jsonString = json.dumps(debuggerPropertiesDict, indent=2) + "\n"
-            with open(filename, "w") as f:
-                f.write(jsonString)
+            if FileSystemUtilities.isRemoteFileName(filename):
+                title = self.tr("Save Remote Debugger Properties")
+                fsInterface.writeFile(filename, jsonString.encode("utf-8"))
+            else:
+                title = self.tr("Save Debugger Properties")
+                with open(filename, "w") as f:
+                    f.write(jsonString)
         except (OSError, TypeError) as err:
             with EricOverridenCursor():
                 EricMessageBox.critical(
                     None,
-                    self.tr("Save Debugger Properties"),
+                    title,
                     self.tr(
                         "<p>The project debugger properties file"
                         " <b>{0}</b> could not be written.</p>"
@@ -93,14 +104,23 @@
         @return flag indicating a successful read
         @rtype bool
         """
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
         try:
-            with open(filename, "r") as f:
-                jsonString = f.read()
+            if FileSystemUtilities.isRemoteFileName(filename):
+                title = self.tr("Read Remote Debugger Properties")
+                jsonString = fsInterface.readFile(filename).decode("utf-8")
+            else:
+                title = self.tr("Read Debugger Properties")
+                with open(filename, "r") as f:
+                    jsonString = f.read()
             debuggerPropertiesDict = json.loads(jsonString)
         except (OSError, json.JSONDecodeError) as err:
             EricMessageBox.critical(
                 None,
-                self.tr("Read Debugger Properties"),
+                title,
                 self.tr(
                     "<p>The project debugger properties file <b>{0}</b>"
                     " could not be read.</p><p>Reason: {1}</p>"
--- a/src/eric7/Project/Project.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Project/Project.py	Fri Feb 23 10:46:46 2024 +0100
@@ -44,6 +44,7 @@
 from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog
 from eric7.EricWidgets.EricProgressDialog import EricProgressDialog
 from eric7.Globals import recentNameProject
+from eric7.RemoteServerInterface import EricServerFileDialog
 from eric7.Sessions.SessionFile import SessionFile
 from eric7.SystemUtilities import (
     FileSystemUtilities,
@@ -158,18 +159,22 @@
     DefaultMake = "make"
     DefaultMakefile = "makefile"
 
-    def __init__(self, parent=None, filename=None):
+    def __init__(self, parent=None, filename=None, remoteServer=None):
         """
         Constructor
 
         @param parent parent widget (usually the ui object)
         @type QWidget
-        @param filename optional filename of a project file to open
-        @type str
+        @param filename optional filename of a project file to open (defaults to None)
+        @type str (optional)
+        @param remoteServer reference to the 'eric-ide' server interface object
+        @type EricServerInterface
         """
         super().__init__(parent)
 
         self.ui = parent
+        self.__remoteServer = remoteServer
+        self.__remotefsInterface = remoteServer.getServiceInterface("FileSystem")
 
         self.__progLanguages = [
             "Python3",
@@ -210,7 +215,7 @@
         else:
             self.vcs = self.initVCS()
 
-        self.__model = ProjectBrowserModel(self)
+        self.__model = ProjectBrowserModel(self, fsInterface=self.__remotefsInterface)
 
         self.codemetrics = None
         self.codecoverage = None
@@ -1098,10 +1103,18 @@
         """
         removed = False
         removelist = []
-        for file in self.__pdata[index]:
-            if not os.path.exists(os.path.join(self.ppath, file)):
-                removelist.append(file)
-                removed = True
+        if FileSystemUtilities.isRemoteFileName(self.ppath):
+            for file in self.__pdata[index]:
+                if not self.__remotefsInterface.exists(
+                    self.__remotefsInterface.join(self.ppath, file)
+                ):
+                    removelist.append(file)
+                    removed = True
+        else:
+            for file in self.__pdata[index]:
+                if not os.path.exists(os.path.join(self.ppath, file)):
+                    removelist.append(file)
+                    removed = True
 
         if removed:
             for file in removelist:
@@ -1110,7 +1123,7 @@
 
     def __readProject(self, fn):
         """
-        Private method to read in a project (.epj) file.
+        Private method to read in a project file (.epj).
 
         @param fn filename of the project file to be read
         @type str
@@ -1121,8 +1134,16 @@
             res = self.__projectFile.readFile(fn)
 
         if res:
-            self.pfile = os.path.abspath(fn)
-            self.ppath = os.path.abspath(os.path.dirname(fn))
+            if FileSystemUtilities.isRemoteFileName(fn):
+                self.pfile = fn
+                self.ppath = self.__remotefsInterface.dirname(fn)
+                self.name = self.__remotefsInterface.splitext(
+                    self.__remotefsInterface.basename(fn)
+                )[0]
+            else:
+                self.pfile = os.path.abspath(fn)
+                self.ppath = os.path.abspath(os.path.dirname(fn))
+                self.name = os.path.splitext(os.path.basename(fn))[0]
 
             # insert filename into list of recently opened projects
             self.__syncRecent()
@@ -1133,15 +1154,22 @@
                 )[0]
             elif self.__pdata["MAINSCRIPT"]:
                 self.translationsRoot = os.path.splitext(self.__pdata["MAINSCRIPT"])[0]
-            if os.path.isdir(os.path.join(self.ppath, self.translationsRoot)):
-                dn = self.translationsRoot
+
+            if FileSystemUtilities.isRemoteFileName(self.ppath):
+                if self.__remotefsInterface.isdir(
+                    self.__remotefsInterface.join(self.ppath, self.translationsRoot)
+                ):
+                    dn = self.translationsRoot
+                else:
+                    dn = self.__remotefsInterface.dirname(self.translationsRoot)
             else:
-                dn = os.path.dirname(self.translationsRoot)
-            if dn not in self.subdirs:
+                if os.path.isdir(os.path.join(self.ppath, self.translationsRoot)):
+                    dn = self.translationsRoot
+                else:
+                    dn = os.path.dirname(self.translationsRoot)
+            if dn and dn not in self.subdirs:
                 self.subdirs.append(dn)
 
-            self.name = os.path.splitext(os.path.basename(fn))[0]
-
             # check, if the files of the project still exist in the
             # project directory
             for fileCategory in self.getFileCategories():
@@ -1150,14 +1178,22 @@
             # get the names of subdirectories the files are stored in
             for fileCategory in [c for c in self.getFileCategories() if c != "OTHERS"]:
                 for fn in self.__pdata[fileCategory]:
-                    dn = os.path.dirname(fn)
-                    if dn not in self.subdirs:
+                    dn = (
+                        self.__remotefsInterface.dirname(fn)
+                        if FileSystemUtilities.isRemoteFileName(fn)
+                        else os.path.dirname(fn)
+                    )
+                    if dn and dn not in self.subdirs:
                         self.subdirs.append(dn)
 
             # get the names of other subdirectories
             for fn in self.__pdata["OTHERS"]:
-                dn = os.path.dirname(fn)
-                if dn not in self.otherssubdirs:
+                dn = (
+                    self.__remotefsInterface.dirname(fn)
+                    if FileSystemUtilities.isRemoteFileName(fn)
+                    else os.path.dirname(fn)
+                )
+                if dn and dn not in self.otherssubdirs:
                     self.otherssubdirs.append(dn)
 
         return res
@@ -1196,9 +1232,16 @@
             res = self.__projectFile.writeFile(fn)
 
         if res:
-            self.pfile = os.path.abspath(fn)
-            self.ppath = os.path.abspath(os.path.dirname(fn))
-            self.name = os.path.splitext(os.path.basename(fn))[0]
+            if FileSystemUtilities.isRemoteFileName(fn):
+                self.pfile = fn
+                self.ppath = self.__remotefsInterface.dirname(fn)
+                self.name = self.__remotefsInterface.splitext(
+                    self.__remotefsInterface.basename(fn)
+                )[0]
+            else:
+                self.pfile = os.path.abspath(fn)
+                self.ppath = os.path.abspath(os.path.dirname(fn))
+                self.name = os.path.splitext(os.path.basename(fn))[0]
             self.setDirty(False)
 
             # insert filename into list of recently opened projects
@@ -1213,10 +1256,22 @@
         if self.pfile is None:
             return
 
-        fn1, ext = os.path.splitext(os.path.basename(self.pfile))
-        fn = os.path.join(self.getProjectManagementDir(), "{0}.eqj".format(fn1))
-        if os.path.exists(fn):
-            self.__userProjectFile.readFile(fn)
+        if FileSystemUtilities.isRemoteFileName(self.pfile):
+            fn1, _ext = self.__remotefsInterface.splitext(
+                self.__remotefsInterface.basename(self.pfile)
+            )
+            fn = self.__remotefsInterface.join(
+                self.getProjectManagementDir(), f"{fn1}.eqj"
+            )
+            if not self.__remotefsInterface.exists(fn):
+                return
+        else:
+            fn1, _ext = os.path.splitext(os.path.basename(self.pfile))
+            fn = os.path.join(self.getProjectManagementDir(), f"{fn1}.eqj")
+            if not os.path.exists(fn):
+                return
+
+        self.__userProjectFile.readFile(fn)
 
     def __writeUserProperties(self):
         """
@@ -1225,8 +1280,16 @@
         if self.pfile is None:
             return
 
-        fn, ext = os.path.splitext(os.path.basename(self.pfile))
-        fn = os.path.join(self.getProjectManagementDir(), "{0}.eqj".format(fn))
+        if FileSystemUtilities.isRemoteFileName(self.pfile):
+            fn1, _ext = self.__remotefsInterface.splitext(
+                self.__remotefsInterface.basename(self.pfile)
+            )
+            fn = self.__remotefsInterface.join(
+                self.getProjectManagementDir(), f"{fn1}.eqj"
+            )
+        else:
+            fn1, _ext = os.path.splitext(os.path.basename(self.pfile))
+            fn = os.path.join(self.getProjectManagementDir(), f"{fn1}.eqj")
 
         with EricOverrideCursor():
             self.__userProjectFile.writeFile(fn)
@@ -1239,9 +1302,18 @@
         if self.pfile is None:
             enable = False
         else:
-            fn, ext = os.path.splitext(os.path.basename(self.pfile))
-            fn_sess = os.path.join(self.getProjectManagementDir(), "{0}.esj".format(fn))
-            enable = os.path.exists(fn_sess)
+            if FileSystemUtilities.isRemoteFileName(self.pfile):
+                fn, _ext = self.__remotefsInterface.splitext(
+                    self.__remotefsInterface.basename(self.pfile)
+                )
+                fn_sess = self.__remotefsInterface.join(
+                    self.getProjectManagementDir(), f"{fn}.esj"
+                )
+                enable = self.__remotefsInterface.exists(fn)
+            else:
+                fn, _ext = os.path.splitext(os.path.basename(self.pfile))
+                fn_sess = os.path.join(self.getProjectManagementDir(), f"{fn}.esj")
+                enable = os.path.exists(fn_sess)
         self.sessActGrp.findChild(QAction, "project_load_session").setEnabled(enable)
         self.sessActGrp.findChild(QAction, "project_delete_session").setEnabled(enable)
 
@@ -1265,12 +1337,22 @@
                 )
             return
 
-        fn1, ext = os.path.splitext(os.path.basename(self.pfile))
-        fn = os.path.join(
-            self.getProjectManagementDir(), "{0}{1}.esj".format(fn1, indicator)
-        )
-        if os.path.exists(fn):
-            self.__sessionFile.readFile(fn)
+        if FileSystemUtilities.isRemoteFileName(self.pfile):
+            fn1, _ext = self.__remotefsInterface.splitext(
+                self.__remotefsInterface.basename(self.pfile)
+            )
+            fn = self.__remotefsInterface.join(
+                self.getProjectManagementDir(), f"{fn1}{indicator}.esj"
+            )
+            if not self.__remotefsInterface.exists(fn):
+                return
+        else:
+            fn1, _ext = os.path.splitext(os.path.basename(self.pfile))
+            fn = os.path.join(self.getProjectManagementDir(), f"{fn1}{indicator}.esj")
+            if not os.path.exists(fn):
+                return
+
+        self.__sessionFile.readFile(fn)
 
     @pyqtSlot()
     def __writeSession(self, quiet=False, indicator=""):
@@ -1292,10 +1374,16 @@
                 )
             return
 
-        fn, ext = os.path.splitext(os.path.basename(self.pfile))
-        fn = os.path.join(
-            self.getProjectManagementDir(), "{0}{1}.esj".format(fn, indicator)
-        )
+        if FileSystemUtilities.isRemoteFileName(self.pfile):
+            fn1, _ext = self.__remotefsInterface.splitext(
+                self.__remotefsInterface.basename(self.pfile)
+            )
+            fn = self.__remotefsInterface.join(
+                self.getProjectManagementDir(), f"{fn1}{indicator}.esj"
+            )
+        else:
+            fn1, _ext = os.path.splitext(os.path.basename(self.pfile))
+            fn = os.path.join(self.getProjectManagementDir(), f"{fn1}{indicator}.esj")
 
         self.__sessionFile.writeFile(fn)
 
@@ -1311,21 +1399,32 @@
             )
             return
 
-        fname, ext = os.path.splitext(os.path.basename(self.pfile))
-
-        fn = os.path.join(self.getProjectManagementDir(), f"{fname}.esj")
-        if os.path.exists(fn):
-            try:
-                os.remove(fn)
-            except OSError:
-                EricMessageBox.critical(
-                    self.ui,
-                    self.tr("Delete Project Session"),
-                    self.tr(
-                        "<p>The project session file <b>{0}</b> could"
-                        " not be deleted.</p>"
-                    ).format(fn),
+        try:
+            if FileSystemUtilities.isRemoteFileName(self.pfile):
+                title = self.tr("Delete Remote Project Session")
+                fname, _ext = self.__remotefsInterface.splitext(
+                    self.__remotefsInterface.basename(self.pfile)
+                )
+                fn = self.__remotefsInterface.join(
+                    self.getProjectManagementDir(), f"{fname}.esj"
                 )
+                if self.__remotefsInterface.exists(fn):
+                    self.__remotefsInterface.remove(fn)
+            else:
+                title = self.tr("Delete Project Session")
+                fname, _ext = os.path.splitext(os.path.basename(self.pfile))
+                fn = os.path.join(self.getProjectManagementDir(), f"{fname}.esj")
+                if os.path.exists(fn):
+                    os.remove(fn)
+        except OSError:
+            EricMessageBox.critical(
+                self.ui,
+                title,
+                self.tr(
+                    "<p>The project session file <b>{0}</b> could"
+                    " not be deleted.</p>"
+                ).format(fn),
+            )
 
     def __readTasks(self):
         """
@@ -1339,10 +1438,22 @@
             )
             return
 
-        base, ext = os.path.splitext(os.path.basename(self.pfile))
-        fn = os.path.join(self.getProjectManagementDir(), "{0}.etj".format(base))
-        if os.path.exists(fn):
-            self.__tasksFile.readFile(fn)
+        if FileSystemUtilities.isRemoteFileName(self.pfile):
+            base, _ext = self.__remotefsInterface.splitext(
+                self.__remotefsInterface.basename(self.pfile)
+            )
+            fn = self.__remotefsInterface.join(
+                self.getProjectManagementDir(), f"{base}.etj"
+            )
+            if not self.__remotefsInterface.exists(fn):
+                return
+        else:
+            base, ext = os.path.splitext(os.path.basename(self.pfile))
+            fn = os.path.join(self.getProjectManagementDir(), f"{base}.etj")
+            if not os.path.exists(fn):
+                return
+
+        self.__tasksFile.readFile(fn)
 
     def writeTasks(self):
         """
@@ -1351,9 +1462,17 @@
         if self.pfile is None:
             return
 
-        fn, ext = os.path.splitext(os.path.basename(self.pfile))
-
-        fn = os.path.join(self.getProjectManagementDir(), "{0}.etj".format(fn))
+        if FileSystemUtilities.isRemoteFileName(self.pfile):
+            base, _ext = self.__remotefsInterface.splitext(
+                self.__remotefsInterface.basename(self.pfile)
+            )
+            fn = self.__remotefsInterface.join(
+                self.getProjectManagementDir(), f"{base}.etj"
+            )
+        else:
+            base, ext = os.path.splitext(os.path.basename(self.pfile))
+            fn = os.path.join(self.getProjectManagementDir(), f"{base}.etj")
+
         self.__tasksFile.writeFile(fn)
 
     def __showContextMenuDebugger(self):
@@ -1364,9 +1483,18 @@
         if self.pfile is None:
             enable = False
         else:
-            fn, ext = os.path.splitext(os.path.basename(self.pfile))
-            fn = os.path.join(self.getProjectManagementDir(), "{0}.edj".format(fn))
-            enable = os.path.exists(fn)
+            if FileSystemUtilities.isRemoteFileName(self.pfile):
+                fn1, _ext = self.__remotefsInterface.splitext(
+                    self.__remotefsInterface.basename(self.pfile)
+                )
+                fn = self.__remotefsInterface.join(
+                    self.getProjectManagementDir(), f"{fn1}.edj"
+                )
+                enable = self.__remotefsInterface.exists(fn)
+            else:
+                fn1, _ext = os.path.splitext(os.path.basename(self.pfile))
+                fn = os.path.join(self.getProjectManagementDir(), f"{fn1}.edj")
+                enable = os.path.exists(fn)
         self.dbgActGrp.findChild(
             QAction, "project_debugger_properties_load"
         ).setEnabled(enable)
@@ -1392,12 +1520,21 @@
                 )
             return
 
-        fn1, ext = os.path.splitext(os.path.basename(self.pfile))
-        fn = os.path.join(self.getProjectManagementDir(), "{0}.edj".format(fn1))
-        if (
-            os.path.exists(fn)
-            and self.__debuggerPropertiesFile.readFile(fn)
-        ):
+        if FileSystemUtilities.isRemoteFileName(self.pfile):
+            fn1, _ext = self.__remotefsInterface.splitext(
+                self.__remotefsInterface.basename(self.pfile)
+            )
+            fn = self.__remotefsInterface.join(
+                self.getProjectManagementDir(), f"{fn1}.edj"
+            )
+            if not self.__remotefsInterface.exists(fn):
+                return
+        else:
+            fn1, _ext = os.path.splitext(os.path.basename(self.pfile))
+            fn = os.path.join(self.getProjectManagementDir(), f"{fn1}.edj")
+            if not os.path.exists(fn):
+                return
+        if self.__debuggerPropertiesFile.readFile(fn):
             self.debugPropertiesLoaded = True
             self.debugPropertiesChanged = False
 
@@ -1419,8 +1556,16 @@
                 )
             return
 
-        fn, ext = os.path.splitext(os.path.basename(self.pfile))
-        fn = os.path.join(self.getProjectManagementDir(), "{0}.edj".format(fn))
+        if FileSystemUtilities.isRemoteFileName(self.pfile):
+            fn1, _ext = self.__remotefsInterface.splitext(
+                self.__remotefsInterface.basename(self.pfile)
+            )
+            fn = self.__remotefsInterface.join(
+                self.getProjectManagementDir(), f"{fn1}.edj"
+            )
+        else:
+            fn1, _ext = os.path.splitext(os.path.basename(self.pfile))
+            fn = os.path.join(self.getProjectManagementDir(), f"{fn1}.edj")
 
         with EricOverrideCursor():
             self.__debuggerPropertiesFile.writeFile(fn)
@@ -1437,21 +1582,32 @@
             )
             return
 
-        fname, ext = os.path.splitext(os.path.basename(self.pfile))
-
-        fn = os.path.join(self.getProjectManagementDir(), f"{fname}.edj")
-        if os.path.exists(fn):
-            try:
-                os.remove(fn)
-            except OSError:
-                EricMessageBox.critical(
-                    self.ui,
-                    self.tr("Delete Debugger Properties"),
-                    self.tr(
-                        "<p>The project debugger properties file"
-                        " <b>{0}</b> could not be deleted.</p>"
-                    ).format(fn),
+        try:
+            if FileSystemUtilities.isRemoteFileName(self.pfile):
+                title = self.tr("Delete Remote Debugger Properties")
+                fname, _ext = self.__remotefsInterface.splitext(
+                    self.__remotefsInterface.basename(self.pfile)
+                )
+                fn = self.__remotefsInterface.join(
+                    self.getProjectManagementDir(), f"{fname}.edj"
                 )
+                if self.__remotefsInterface.exists(fn):
+                    self.__remotefsInterface.remove(fn)
+            else:
+                title = self.tr("Delete Debugger Properties")
+                fname, _ext = os.path.splitext(os.path.basename(self.pfile))
+                fn = os.path.join(self.getProjectManagementDir(), f"{fname}.edj")
+                if os.path.exists(fn):
+                    os.remove(fn)
+        except OSError:
+            EricMessageBox.critical(
+                self.ui,
+                title,
+                self.tr(
+                    "<p>The project debugger properties file"
+                    " <b>{0}</b> could not be deleted.</p>"
+                ).format(fn),
+            )
 
     def __initDebugProperties(self):
         """
@@ -1672,16 +1828,34 @@
         for langFile in self.__pdata["TRANSLATIONS"][:]:
             qmFile = self.__binaryTranslationFile(langFile)
             if qmFile:
-                if qmFile not in self.__pdata["TRANSLATIONS"] and os.path.exists(
-                    os.path.join(self.ppath, qmFile)
-                ):
-                    self.appendFile(qmFile)
-                if tbPath:
-                    qmFile = os.path.join(tbPath, os.path.basename(qmFile))
+                if FileSystemUtilities.isRemoteFileName(self.ppath):
+                    if qmFile not in self.__pdata[
+                        "TRANSLATIONS"
+                    ] and self.__remotefsInterface.exists(
+                        self.__remotefsInterface.join(self.ppath, qmFile)
+                    ):
+                        self.appendFile(qmFile)
+                    if tbPath:
+                        qmFile = self.__remotefsInterface.join(
+                            tbPath, self.__remotefsInterface.basename(qmFile)
+                        )
+                        if qmFile not in self.__pdata[
+                            "TRANSLATIONS"
+                        ] and self.__remotefsInterface.exists(
+                            self.__remotefsInterface.join(self.ppath, qmFile)
+                        ):
+                            self.appendFile(qmFile)
+                else:
                     if qmFile not in self.__pdata["TRANSLATIONS"] and os.path.exists(
                         os.path.join(self.ppath, qmFile)
                     ):
                         self.appendFile(qmFile)
+                    if tbPath:
+                        qmFile = os.path.join(tbPath, os.path.basename(qmFile))
+                        if qmFile not in self.__pdata[
+                            "TRANSLATIONS"
+                        ] and os.path.exists(os.path.join(self.ppath, qmFile)):
+                            self.appendFile(qmFile)
 
     def removeLanguageFile(self, langFile):
         """
@@ -1700,12 +1874,18 @@
         if qmFile:
             with contextlib.suppress(ValueError):
                 if self.__pdata["TRANSLATIONSBINPATH"]:
-                    qmFile = self.getRelativePath(
-                        os.path.join(
+                    if FileSystemUtilities.isRemoteFileName(self.ppath):
+                        qmFile = self.__remotefsInterface.join(
                             self.__pdata["TRANSLATIONSBINPATH"],
-                            os.path.basename(qmFile),
+                            self.__remotefsInterface.basename(qmFile),
                         )
-                    )
+                    else:
+                        qmFile = self.getRelativePath(
+                            os.path.join(
+                                self.__pdata["TRANSLATIONSBINPATH"],
+                                os.path.basename(qmFile),
+                            )
+                        )
                 self.__model.removeItem(qmFile)
                 self.__pdata["TRANSLATIONS"].remove(qmFile)
         self.setDirty(True)
@@ -1721,13 +1901,18 @@
         qmFile = self.__binaryTranslationFile(langFile)
 
         try:
-            fn = os.path.join(self.ppath, langFile)
-            if os.path.exists(fn):
-                os.remove(fn)
+            if FileSystemUtilities.isRemoteFileName(self.ppath):
+                fn = self.__remotefsInterface.join(self.ppath, langFile)
+                if self.__remotefsInterface.exists(fn):
+                    self.__remotefsInterface.remove(fn)
+            else:
+                fn = os.path.join(self.ppath, langFile)
+                if os.path.exists(fn):
+                    os.remove(fn)
         except OSError as err:
             EricMessageBox.critical(
                 self.ui,
-                self.tr("Delete translation"),
+                self.tr("Delete Translation"),
                 self.tr(
                     "<p>The selected translation file <b>{0}</b> could not be"
                     " deleted.</p><p>Reason: {1}</p>"
@@ -1778,14 +1963,22 @@
 
         # make it relative to the project root, if it starts with that path
         # assume relative paths are relative to the project root
-        newfn = self.getRelativePath(fn) if os.path.isabs(fn) else fn
-        newdir = os.path.dirname(newfn)
+        if FileSystemUtilities.isRemoteFileName(self.ppath):
+            newfn = self.getRelativePath(fn) if fn.startswith(self.ppath) else fn
+            newdir = self.__remotefsInterface.dirname(newfn)
+        else:
+            newfn = self.getRelativePath(fn) if os.path.isabs(fn) else fn
+            newdir = os.path.dirname(newfn)
 
         if isSourceFile:
             filetype = "SOURCES"
         else:
             filetype = "OTHERS"
-            bfn = os.path.basename(newfn)
+            bfn = (
+                self.__remotefsInterface.basename(newfn)
+                if FileSystemUtilities.isRemoteFileName(self.ppath)
+                else os.path.basename(newfn)
+            )
             if fnmatch.fnmatch(bfn, "*.ts") or fnmatch.fnmatch(bfn, "*.qm"):
                 filetype = "TRANSLATIONS"
             else:
@@ -1842,6 +2035,7 @@
         @param startdir start directory for the selection dialog
         @type str
         """
+        # TODO: adapt to remote server
         from .AddFileDialog import AddFileDialog
 
         if not startdir:
@@ -1906,6 +2100,7 @@
         @param quiet flag indicating quiet operations
         @type bool
         """
+        # TODO: adapt to remote server
         # get all relevant filename patterns
         patterns = []
         ignorePatterns = []
@@ -1989,6 +2184,7 @@
         @param target target directory
         @type str
         """
+        # TODO: adapt to remote server
         # first perform the addition of source
         self.__addSingleDirectory(filetype, source, target, True)
 
@@ -2027,6 +2223,7 @@
         @param startdir start directory for the selection dialog
         @type str
         """
+        # TODO: adapt to remote server
         from .AddDirectoryDialog import AddDirectoryDialog
 
         if not startdir:
@@ -2067,6 +2264,7 @@
         @param fn file name or directory name to add
         @type str
         """
+        # TODO: adapt to remote server
         if fn:
             # if it is below the project directory, make it relative to that
             fn = self.getRelativePath(fn)
@@ -2112,6 +2310,7 @@
         @return flag indicating success
         @rtype bool
         """
+        # TODO: adapt to remote server
         fn = self.getRelativePath(oldfn)
         isSourceFile = fn in self.__pdata["SOURCES"]
 
@@ -2170,6 +2369,7 @@
                 even if it doesn't have the source extension
         @type bool
         """
+        # TODO: adapt to remote server
         fn = self.getRelativePath(oldname)
         if os.path.dirname(oldname) == os.path.dirname(newname):
             if self.__isInPdata(oldname):
@@ -2192,6 +2392,7 @@
         @return list of files starting with a common prefix
         @rtype list of str
         """
+        # TODO: adapt to remote server
         filelist = []
         start = self.getRelativePath(start)
         for fileCategory in [
@@ -2461,6 +2662,10 @@
             self.reloadAct.setEnabled(True)
             self.closeAct.setEnabled(True)
             self.saveasAct.setEnabled(True)
+            self.saveasRemoteAct.setEnabled(
+                self.__remoteServer.isServerConnected()
+                and FileSystemUtilities.isRemoteFileName(self.pfile)
+            )
             self.actGrp2.setEnabled(True)
             self.propsAct.setEnabled(True)
             self.userPropsAct.setEnabled(True)
@@ -3192,6 +3397,10 @@
                     self.reloadAct.setEnabled(True)
                     self.closeAct.setEnabled(True)
                     self.saveasAct.setEnabled(True)
+                    self.saveasRemoteAct.setEnabled(
+                        self.__remoteServer.isServerConnected()
+                        and FileSystemUtilities.isRemoteFileName(self.pfile)
+                    )
                     self.actGrp2.setEnabled(True)
                     self.propsAct.setEnabled(True)
                     self.userPropsAct.setEnabled(True)
@@ -3331,7 +3540,7 @@
             if fpath.exists():
                 res = EricMessageBox.yesNo(
                     self.ui,
-                    self.tr("Save File"),
+                    self.tr("Save Project"),
                     self.tr(
                         """<p>The file <b>{0}</b> already exists."""
                         """ Overwrite it?</p>"""
@@ -3341,7 +3550,6 @@
                 if not res:
                     return False
 
-            self.name = fpath.stem
             ok = self.__writeProject(str(fpath))
 
             if ok:
@@ -3476,6 +3684,7 @@
         self.__initData()
         self.reloadAct.setEnabled(False)
         self.closeAct.setEnabled(False)
+        self.saveasRemoteAct.setEnabled(False)
         self.saveasAct.setEnabled(False)
         self.saveAct.setEnabled(False)
         self.actGrp2.setEnabled(False)
@@ -3792,8 +4001,7 @@
         @rtype str
         """
         if self.pfile:
-            name = os.path.splitext(self.pfile)[0]
-            return os.path.basename(name)
+            return self.name
         else:
             return ""
 
@@ -3804,7 +4012,10 @@
         @return path of the management directory
         @rtype str
         """
-        return os.path.join(self.ppath, ".eric7project")
+        if FileSystemUtilities.isRemoteFileName(self.ppath):
+            return self.__remotefsInterface.join(self.ppath, ".eric7project")
+        else:
+            return os.path.join(self.ppath, ".eric7project")
 
     def createProjectManagementDir(self):
         """
@@ -3814,8 +4025,10 @@
         """
         # create management directory if not present
         mgmtDir = self.getProjectManagementDir()
-        if not os.path.exists(mgmtDir):
-            os.makedirs(mgmtDir)
+        if FileSystemUtilities.isRemoteFileName(mgmtDir):
+            self.__remotefsInterface.makedirs(mgmtDir, exist_ok=True)
+        else:
+            os.makedirs(mgmtDir, exist_ok=True)
 
     def getHash(self):
         """
@@ -3841,7 +4054,13 @@
             return ""
 
         try:
-            return str(pathlib.Path(path).relative_to(self.ppath))
+            if FileSystemUtilities.isRemoteFileName(self.ppath):
+                if self.__remotefsInterface.separator() == "\\":
+                    return str(pathlib.PureWindowsPath(path).relative_to(self.ppath))
+                else:
+                    return str(pathlib.PurePosixPath(path).relative_to(self.ppath))
+            else:
+                return str(pathlib.PurePath(path).relative_to(self.ppath))
         except ValueError:
             return path
 
@@ -3856,7 +4075,12 @@
             belong to the project
         @rtype str
         """
-        return FileSystemUtilities.fromNativeSeparators(self.getRelativePath(path))
+        if FileSystemUtilities.isRemoteFileName(self.ppath):
+            return self.__remotefsInterface.fromNativeSeparators(
+                self.getRelativePath(path)
+            )
+        else:
+            return FileSystemUtilities.fromNativeSeparators(self.getRelativePath(path))
 
     def getAbsolutePath(self, fn):
         """
@@ -3868,8 +4092,11 @@
         @return absolute path
         @rtype str
         """
-        if not os.path.isabs(fn):
-            fn = os.path.join(self.ppath, fn)
+        if not fn.startswith(self.ppath):
+            if FileSystemUtilities.isRemoteFileName(self.ppath):
+                fn = self.__remotefsInterface.join(self.ppath, fn)
+            else:
+                fn = os.path.join(self.ppath, fn)
         return fn
 
     def getAbsoluteUniversalPath(self, fn):
@@ -3882,8 +4109,15 @@
         @return absolute path
         @rtype str
         """
-        if not os.path.isabs(fn):
-            fn = os.path.join(self.ppath, FileSystemUtilities.toNativeSeparators(fn))
+        if not fn.startswith(self.ppath):
+            if FileSystemUtilities.isRemoteFileName(self.ppath):
+                fn = self.__remotefsInterface.join(
+                    self.ppath, self.__remotefsInterface.fromNativeSeparators(fn)
+                )
+            else:
+                fn = os.path.join(
+                    self.ppath, FileSystemUtilities.fromNativeSeparators(fn)
+                )
         return fn
 
     def getEolString(self):
@@ -4106,7 +4340,7 @@
         @return flag indicating membership
         @rtype bool
         """
-        newfn = os.path.abspath(fn)
+        newfn = fn if FileSystemUtilities.isRemoteFileName(fn) else os.path.abspath(fn)
         newfn = self.getRelativePath(newfn)
         if newfn in self.__pdata[group] or (
             group == "OTHERS"
@@ -4192,6 +4426,25 @@
         act.triggered.connect(self.openProject)
         self.actions.append(act)
 
+        self.openRemoteAct = EricAction(
+            self.tr("Open remote project"),
+            EricPixmapCache.getIcon("projectOpen-remote"),
+            self.tr("Open (Remote)..."),
+            0,
+            0,
+            self.actGrp1,
+            "project_open_remote",
+        )
+        self.openRemoteAct.setStatusTip(self.tr("Open an existing remote project"))
+        self.openRemoteAct.setWhatsThis(
+            self.tr(
+                "<b>Open (Remote)...</b><p>This opens an existing remote project.</p>"
+            )
+        )
+        self.openRemoteAct.triggered.connect(self.__openRemoteProject)
+        self.actions.append(self.openRemoteAct)
+        self.openRemoteAct.setEnabled(False)  # server is not connected initially
+
         self.reloadAct = EricAction(
             self.tr("Reload project"),
             EricPixmapCache.getIcon("projectReload"),
@@ -4259,6 +4512,28 @@
         self.saveasAct.triggered.connect(self.saveProjectAs)
         self.actions.append(self.saveasAct)
 
+        self.saveasRemoteAct = EricAction(
+            self.tr("Save project as (Remote)"),
+            EricPixmapCache.getIcon("projectSaveAs-remote"),
+            self.tr("Save as (Remote)..."),
+            0,
+            0,
+            self,
+            "project_save_as_remote",
+        )
+        self.saveasRemoteAct.setStatusTip(
+            self.tr("Save the current project to a new remote file")
+        )
+        self.saveasRemoteAct.setWhatsThis(
+            self.tr(
+                """<b>Save as (Remote)</b>"""
+                """<p>This saves the current project to a new remote file.</p>"""
+            )
+        )
+        self.saveasRemoteAct.triggered.connect(self.__saveRemoteProjectAs)
+        self.actions.append(self.saveasRemoteAct)
+        self.saveasRemoteAct.setEnabled(False)  # server is not connected initially
+
         ###################################################################
         ## Project management actions
         ###################################################################
@@ -5345,6 +5620,7 @@
         menu.addSeparator()
         menu.addAction(self.saveAct)
         menu.addAction(self.saveasAct)
+        menu.addAction(self.saveasRemoteAct)
         menu.addSeparator()
         menu.addActions(self.actGrp2.actions())
         menu.addSeparator()
@@ -5417,6 +5693,7 @@
         tb.addSeparator()
         tb.addAction(self.saveAct)
         tb.addAction(self.saveasAct)
+        tb.addAction(self.saveasRemoteAct)
 
         toolbarManager.addToolBar(tb, tb.windowTitle())
         toolbarManager.addAction(self.addFilesAct, tb.windowTitle())
@@ -7159,6 +7436,90 @@
             # the configuration file does not exist or is invalid JSON
             self.__initVenvConfiguration()
 
+    #############################################################################
+    ## Below are methods implementing the support for 'eric-ide server projects
+    #############################################################################
+
+    @pyqtSlot(bool)
+    def remoteConnectionChanged(self, connected):
+        """
+        Public slot to handle a change of the 'eric-ide' server connection state.
+
+        @param connected flag indicating the connection state
+        @type bool
+        """
+        self.openRemoteAct.setEnabled(connected)
+        self.saveasRemoteAct.setEnabled(
+            connected
+            and self.opened
+            and FileSystemUtilities.isRemoteFileName(self.pfile)
+        )
+
+    @pyqtSlot()
+    def __openRemoteProject(self):
+        """
+        Private slot to open a project of an 'eric-ide' server.
+        """
+        fn = EricServerFileDialog.getOpenFileName(
+            self.parent(),
+            self.tr("Open Remote Project"),
+            "",
+            self.tr("Project Files (*.epj)"),
+        )
+        if fn:
+            self.openProject(fn=fn)
+
+    @pyqtSlot()
+    def __saveRemoteProjectAs(self):
+        """
+        Private slot to save the current remote project to different remote file.
+        """
+        defaultFilter = self.tr("Project Files (*.epj)")
+        defaultPath = self.ppath if self.ppath else ""
+        fn, selectedFilter = EricServerFileDialog.getSaveFileNameAndFilter(
+            self.parent(),
+            self.tr("Save Remote Project"),
+            defaultPath,
+            self.tr("Project Files (*.epj)"),
+            defaultFilter,
+            EricFileDialog.DontConfirmOverwrite,
+        )
+
+        if fn:
+            fname, ext = self.__remotefsInterface.splitext(fn)
+            if not ext:
+                ex = selectedFilter.split("(*")[1].split(")")[0]
+                if ex:
+                    fn = f"{fname}{ex}"
+            if self.__remotefsInterface.exists(fn):
+                res = EricMessageBox.yesNo(
+                    self.ui,
+                    self.tr("Save Remote Project"),
+                    self.tr(
+                        """<p>The file <b>{0}</b> already exists."""
+                        """ Overwrite it?</p>"""
+                    ).format(fn),
+                    icon=EricMessageBox.Warning,
+                )
+                if not res:
+                    return
+
+            ok = self.__writeProject(fn)
+
+            if ok:
+                # create management directory if not present
+                self.createProjectManagementDir()
+
+                # now save the tasks
+                self.writeTasks()
+
+            self.sessActGrp.setEnabled(ok)
+            self.menuSessionAct.setEnabled(ok)
+            self.projectClosedHooks.emit()
+            self.projectClosed.emit(False)
+            self.projectOpenedHooks.emit()
+            self.projectOpened.emit()
+
 
 #
 # eflag: noqa = M601
--- a/src/eric7/Project/ProjectBrowserModel.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Project/ProjectBrowserModel.py	Fri Feb 23 10:46:46 2024 +0100
@@ -138,7 +138,9 @@
     Class implementing the data structure for project browser directory items.
     """
 
-    def __init__(self, parent, dinfo, projectType, full=True, bold=False):
+    def __init__(
+        self, parent, dinfo, projectType, full=True, bold=False, fsInterface=None
+    ):
         """
         Constructor
 
@@ -152,8 +154,11 @@
         @type bool
         @param bold flag indicating a highlighted font
         @type bool
+        @param fsInterface reference to the 'eric-ide' server file system interface
+            (defaults to None)
+        @type EricServerFileSystemInterface (optional)
         """
-        BrowserDirectoryItem.__init__(self, parent, dinfo, full)
+        BrowserDirectoryItem.__init__(self, parent, dinfo, full, fsInterface)
         ProjectBrowserItemMixin.__init__(self, projectType, bold)
 
         self.type_ = BrowserItemType.PbDirectory
@@ -165,7 +170,14 @@
     """
 
     def __init__(
-        self, parent, finfo, projectType, full=True, bold=False, sourceLanguage=""
+        self,
+        parent,
+        finfo,
+        projectType,
+        full=True,
+        bold=False,
+        sourceLanguage="",
+        fsInterface=None,
     ):
         """
         Constructor
@@ -182,8 +194,11 @@
         @type bool
         @param sourceLanguage source code language of the project
         @type str
+        @param fsInterface reference to the 'eric-ide' server file system interface
+            (defaults to None)
+        @type EricServerFileSystemInterface (optional)
         """
-        BrowserFileItem.__init__(self, parent, finfo, full, sourceLanguage)
+        BrowserFileItem.__init__(self, parent, finfo, full, sourceLanguage, fsInterface)
         ProjectBrowserItemMixin.__init__(self, projectType, bold)
 
         self.type_ = BrowserItemType.PbFile
@@ -198,12 +213,15 @@
 
     vcsStateChanged = pyqtSignal(str)
 
-    def __init__(self, parent):
+    def __init__(self, parent, fsInterface=None):
         """
         Constructor
 
         @param parent reference to parent object
         @type Project.Project
+        @param fsInterface reference to the 'eric-ide' server interface object
+            (defaults to None)
+        @type EricServerFileSystemInterface (optional)
         """
         super().__init__(parent, nopopulate=True)
 
@@ -215,6 +233,8 @@
         self.project = parent
         self.__projectBrowser = None
 
+        self.__remotefsInterface = fsInterface
+
         self.watchedItems = {}
         self.__watcherActive = True
         self.watcher = QFileSystemWatcher(self)
@@ -418,7 +438,16 @@
             )
 
             for fn in self.project.getProjectData(dataKey=fileCategory):
-                fname = os.path.join(self.project.ppath, fn)
+                fname = (
+                    self.__remotefsInterface.join(self.project.ppath, fn)
+                    if FileSystemUtilities.isRemoteFileName(self.project.ppath)
+                    else os.path.join(self.project.ppath, fn)
+                )
+                isdir = (
+                    self.__remotefsInterface.isdir(fname)
+                    if FileSystemUtilities.isRemoteFileName(fname)
+                    else os.path.isdir(fname)
+                )
                 parentItem, dt = self.findParentItemByName(
                     self.__projectBrowser.getProjectBrowserFilter(fileCategory), fn
                 )
@@ -429,8 +458,9 @@
                         self.__projectBrowser.getProjectBrowserFilter(fileCategory),
                         False,
                         bold,
+                        fsInterface=self.__remotefsInterface,
                     )
-                    if os.path.isdir(fname)
+                    if isdir
                     else ProjectBrowserFileItem(
                         parentItem,
                         fname,
@@ -438,6 +468,7 @@
                         False,
                         bold,
                         sourceLanguage=sourceLanguage,
+                        fsInterface=self.__remotefsInterface,
                     )
                 )
                 self._addItem(itm, parentItem)
--- a/src/eric7/Project/ProjectFile.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Project/ProjectFile.py	Fri Feb 23 10:46:46 2024 +0100
@@ -17,6 +17,7 @@
 from eric7 import Preferences
 from eric7.EricGui.EricOverrideCursor import EricOverridenCursor
 from eric7.EricWidgets import EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
 from eric7.SystemUtilities import FileSystemUtilities
 
 Project = typing.TypeVar("Project")
@@ -48,6 +49,11 @@
         @return flag indicating a successful write
         @rtype bool
         """
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+        isRemote = FileSystemUtilities.isRemoteFileName(filename)
+
         projectDict = {
             "header": {
                 "comment": "eric project file for project {0}".format(
@@ -85,19 +91,28 @@
             "SOURCESDIR",
         ):
             with contextlib.suppress(KeyError):
-                projectDict["project"][key] = FileSystemUtilities.fromNativeSeparators(
-                    projectDict["project"][key]
+                projectDict["project"][key] = (
+                    fsInterface.fromNativeSeparators(projectDict["project"][key])
+                    if isRemote
+                    else FileSystemUtilities.fromNativeSeparators(
+                        projectDict["project"][key]
+                    )
                 )
 
         try:
             jsonString = json.dumps(projectDict, indent=2, sort_keys=True) + "\n"
-            with open(filename, "w", newline="") as f:
-                f.write(jsonString)
+            if isRemote:
+                title = self.tr("Save Remote Project File")
+                fsInterface.writeFile(filename, jsonString.encode("utf-8"))
+            else:
+                title = self.tr("Save Project File")
+                with open(filename, "w", newline="") as f:
+                    f.write(jsonString)
         except (OSError, TypeError) as err:
             with EricOverridenCursor():
                 EricMessageBox.critical(
                     None,
-                    self.tr("Save Project File"),
+                    title,
                     self.tr(
                         "<p>The project file <b>{0}</b> could not be "
                         "written.</p><p>Reason: {1}</p>"
@@ -116,14 +131,24 @@
         @return flag indicating a successful read
         @rtype bool
         """
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
+        isRemote = FileSystemUtilities.isRemoteFileName(filename)
         try:
-            with open(filename, "r") as f:
-                jsonString = f.read()
+            if isRemote:
+                title = self.tr("Read Remote Project File")
+                jsonString = fsInterface.readFile(filename).decode("utf-8")
+            else:
+                title = self.tr("Read Project File")
+                with open(filename, "r") as f:
+                    jsonString = f.read()
             projectDict = json.loads(jsonString)
         except (OSError, json.JSONDecodeError) as err:
             EricMessageBox.critical(
                 None,
-                self.tr("Read Project File"),
+                title,
                 self.tr(
                     "<p>The project file <b>{0}</b> could not be "
                     "read.</p><p>Reason: {1}</p>"
@@ -134,10 +159,17 @@
         # modify paths to contain native separators
         for key in self.__project.getFileCategories() + ["TRANSLATIONEXCEPTIONS"]:
             with contextlib.suppress(KeyError):
-                projectDict["project"][key] = [
-                    FileSystemUtilities.toNativeSeparators(f)
-                    for f in projectDict["project"][key]
-                ]
+                projectDict["project"][key] = (
+                    [
+                        fsInterface.toNativeSeparators(f)
+                        for f in projectDict["project"][key]
+                    ]
+                    if isRemote
+                    else [
+                        FileSystemUtilities.toNativeSeparators(f)
+                        for f in projectDict["project"][key]
+                    ]
+                )
         for key in (
             "SPELLWORDS",
             "SPELLEXCLUDES",
@@ -148,8 +180,12 @@
             "SOURCESDIR",
         ):
             with contextlib.suppress(KeyError):
-                projectDict["project"][key] = FileSystemUtilities.toNativeSeparators(
-                    projectDict["project"][key]
+                projectDict["project"][key] = (
+                    fsInterface.toNativeSeparators(projectDict["project"][key])
+                    if isRemote
+                    else FileSystemUtilities.toNativeSeparators(
+                        projectDict["project"][key]
+                    )
                 )
 
         self.__project.setProjectData(projectDict["project"])
--- a/src/eric7/Project/UserProjectFile.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Project/UserProjectFile.py	Fri Feb 23 10:46:46 2024 +0100
@@ -16,6 +16,8 @@
 from eric7 import Preferences
 from eric7.EricGui.EricOverrideCursor import EricOverridenCursor
 from eric7.EricWidgets import EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities
 
 Project = typing.TypeVar("Project")
 
@@ -47,6 +49,10 @@
         @return flag indicating a successful write
         @rtype bool
         """
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
         userProjectDict = {
             "header": {
                 "comment": "eric user project file for project {0}".format(
@@ -62,13 +68,18 @@
 
         try:
             jsonString = json.dumps(userProjectDict, indent=2) + "\n"
-            with open(filename, "w") as f:
-                f.write(jsonString)
+            if FileSystemUtilities.isRemoteFileName(filename):
+                title = self.tr("Save Remote User Project Properties")
+                fsInterface.writeFile(filename, jsonString.encode("utf-8"))
+            else:
+                title = self.tr("Save User Project Properties")
+                with open(filename, "w") as f:
+                    f.write(jsonString)
         except (OSError, TypeError) as err:
             with EricOverridenCursor():
                 EricMessageBox.critical(
                     None,
-                    self.tr("Save User Project Properties"),
+                    title,
                     self.tr(
                         "<p>The user specific project properties file"
                         " <b>{0}</b> could not be written.</p>"
@@ -89,14 +100,23 @@
         @return flag indicating a successful read
         @rtype bool
         """
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
         try:
-            with open(filename, "r") as f:
-                jsonString = f.read()
+            if FileSystemUtilities.isRemoteFileName(filename):
+                title = self.tr("Read Remote User Project Properties")
+                jsonString = fsInterface.readFile(filename).decode("utf-8")
+            else:
+                title = self.tr("Read User Project Properties")
+                with open(filename, "r") as f:
+                    jsonString = f.read()
             userProjectDict = json.loads(jsonString)
         except (OSError, json.JSONDecodeError) as err:
             EricMessageBox.critical(
                 None,
-                self.tr("Read User Project Properties"),
+                title,
                 self.tr(
                     "<p>The user specific project properties file <b>{0}</b>"
                     " could not be read.</p><p>Reason: {1}</p>"
--- a/src/eric7/QScintilla/Editor.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/QScintilla/Editor.py	Fri Feb 23 10:46:46 2024 +0100
@@ -67,6 +67,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
@@ -264,6 +265,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
@@ -478,7 +483,10 @@
                         if not res:
                             raise OSError()
 
-                    self.readFile(self.fileName, True)
+                    self.readFile(self.fileName, createIt=True)
+
+                elif FileSystemUtilities.isRemoteFileName(self.fileName):
+                    self.readFile(self.fileName, createIt=True, isRemote=True)
 
                 self.__bindLexer(self.fileName)
                 self.__bindCompleter(self.fileName)
@@ -1050,6 +1058,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..."),
@@ -1177,7 +1190,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
         )
@@ -2156,8 +2171,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())
@@ -3455,9 +3470,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)
@@ -3485,7 +3498,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.
 
@@ -3499,26 +3512,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>"
@@ -3548,7 +3581,7 @@
 
             self.extractTasks()
 
-            self.lastModified = pathlib.Path(fn).stat().st_mtime
+            self.recordModificationTime(filename=fn)
 
     @pyqtSlot()
     def __convertTabs(self):
@@ -3600,7 +3633,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:
@@ -3622,7 +3659,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)
@@ -3642,28 +3679,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,
+                    createBackup=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
         """
@@ -3677,7 +3727,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])
@@ -3689,14 +3744,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("."):
@@ -3707,10 +3774,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),
@@ -3743,21 +3813,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):
@@ -3767,7 +3837,7 @@
         if saveas or self.fileName == "":
             saveas = True
 
-            fn = self.__getSaveFileName(path)
+            fn = self.__getSaveFileName(path=path, remote=remote)
             if not fn:
                 return False
 
@@ -3786,13 +3856,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)
@@ -3814,7 +3884,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)
@@ -3824,21 +3894,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):
         """
@@ -3899,7 +3969,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)
 
@@ -6329,6 +6399,9 @@
             not self.isModified() and 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())
@@ -6346,6 +6419,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())
@@ -6437,6 +6513,7 @@
         )
         self.coverageHideAnnotationMenuAct.setEnabled(len(self.notcoveredMarkers) > 0)
 
+        # TODO: disable action in Radon plugin for server files
         self.showMenu.emit("Show", self.menuShow, self)
 
     @pyqtSlot()
@@ -6607,6 +6684,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.
@@ -6962,9 +7048,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)
@@ -8008,9 +8111,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):
@@ -8020,7 +8133,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)
@@ -8193,9 +8310,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()
@@ -8217,9 +8332,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):
@@ -8227,13 +8351,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"""
@@ -8254,23 +8374,79 @@
                     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)
+                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()
@@ -8290,11 +8466,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)
@@ -8569,11 +8740,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"),
@@ -9695,7 +9873,7 @@
         """
         editorConfig = {}
 
-        if fileName and FileSystemUtilities.isPlainFileName(self.fileName):
+        if fileName and FileSystemUtilities.isPlainFileName(fileName):
             try:
                 editorConfig = editorconfig.get_properties(fileName)
             except editorconfig.EditorConfigError:
@@ -9733,16 +9911,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"]
--- a/src/eric7/QScintilla/Shell.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/QScintilla/Shell.py	Fri Feb 23 10:46:46 2024 +0100
@@ -391,10 +391,16 @@
         self.lmenu.clear()
         venvManager = ericApp().getObject("VirtualEnvManager")
         for venvName in sorted(venvManager.getVirtualenvNames()):
-            self.lmenu.addAction(venvName)
+            act = self.lmenu.addAction(venvName)
+            act.setData(venvName)
         if self.__project.isOpen():
             self.lmenu.addSeparator()
-            self.lmenu.addAction(self.tr("Project"))
+            act = self.lmenu.addAction(self.tr("Project"))
+            act.setData("<<project>>")
+        if ericApp().getObject("EricServer").isServerConnected():
+            self.lmenu.addSeparator()
+            act = self.lmenu.addAction(self.tr("eric-ide Server"))
+            act.setData("<<eric-server>>")
 
     def __resizeLinenoMargin(self):
         """
@@ -2153,13 +2159,15 @@
         @param action context menu action that was triggered
         @type QAction
         """
-        venvName = action.text()
-        if venvName == self.tr("Project"):
+        venvName = action.data()
+        if venvName == "<<project>>":
             if self.__project.isOpen():
                 self.__currentWorkingDirectory = self.__project.getProjectPath()
             self.dbs.startClient(
                 False, forProject=True, workingDir=self.__currentWorkingDirectory
             )
+        elif venvName == "<<eric-server>>":
+            self.dbs.startClient(False, startRemote=True)
         else:
             self.dbs.startClient(False, venvName=venvName)
         self.__getBanner()
@@ -2634,6 +2642,27 @@
             self.dbs.startClient(False)
             self.__getBanner()
 
+    #################################################################
+    ## eric-ide Server Support
+    #################################################################
+
+    @pyqtSlot(bool)
+    def remoteConnectionChanged(self, connected):
+        """
+        Public slot handling a change of the connection state to an eric-ide server.
+
+        @param connected flag indicating the connection state
+        @type bool
+        """
+        if connected:
+            if Preferences.getEricServer("AutostartShell"):
+                self.dbs.startClient(False, startRemote=True)
+        else:
+            if self.__currentVenv == self.dbs.getEricServerEnvironmentString():
+                # start default backend
+                self.dbs.startClient(False)
+                self.__getBanner()
+
 
 #
 # eflag: noqa = M601
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/EricRequestCategory.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing an enum for the various service categories.
+"""
+
+import enum
+
+
+class EricRequestCategory(enum.IntEnum):
+    """
+    Class defining the service categories of the eric remote server.
+    """
+
+    FileSystem = 0
+    Project = 1
+    Debugger = 2
+    Coverage = 3
+
+    Echo = 253
+    Server = 254
+    Error = 255  # only sent by the server to report an issue
+
+    # user/plugins may define own categories starting with this value
+    UserCategory = 1024
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/EricServer.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,562 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the eric remote server.
+"""
+
+import io
+import json
+import selectors
+import socket
+import struct
+import sys
+import traceback
+import types
+import zlib
+
+from eric7.UI.Info import Version
+
+from .EricRequestCategory import EricRequestCategory
+from .EricServerCoverageRequestHandler import EricServerCoverageRequestHandler
+from .EricServerDebuggerRequestHandler import EricServerDebuggerRequestHandler
+from .EricServerFileSystemRequestHandler import EricServerFileSystemRequestHandler
+
+# TODO: remove dependency on 'eric7.UI.Info'
+
+
+class EricServer:
+    """
+    Class implementing the eric remote server.
+    """
+
+    def __init__(self, port=42024, useIPv6=False):
+        """
+        Constructor
+
+        @param port port to listen on (defaults to 42024)
+        @type int (optional)
+        @param useIPv6 flag indicating to use IPv6 protocol (defaults to False)
+        @type bool (optional)
+        """
+        self.__requestCategoryHandlerRegistry = {}
+        # Dictionary containing the defined and registered request category
+        # handlers. The key is the request category and the value is the respective
+        # handler method. This method must have the signature:
+        #     handler(request:str, params:dict, reqestUuid:str) -> None
+        self.__registerInternalHandlers()
+
+        self.__connection = None
+
+        self.__selector = selectors.DefaultSelector()
+
+        # create and register the 'Debugger' request handler
+        self.__debuggerRequestHandler = EricServerDebuggerRequestHandler(self)
+        self.registerRequestHandler(
+            EricRequestCategory.Debugger,
+            self.__debuggerRequestHandler.handleRequest,
+        )
+
+        # create and register the 'File System' request handler
+        self.__fileSystemRequestHandler = EricServerFileSystemRequestHandler(self)
+        self.registerRequestHandler(
+            EricRequestCategory.FileSystem,
+            self.__fileSystemRequestHandler.handleRequest,
+        )
+
+        # create and register the 'Coverage' request handler
+        self.__coverageRequestHandler = EricServerCoverageRequestHandler(self)
+        self.registerRequestHandler(
+            EricRequestCategory.Coverage,
+            self.__coverageRequestHandler.handleRequest,
+        )
+
+        # TODO: 'Project' handler not implemented yet
+        # TODO: implement an 'EditorConfig' handler (?)
+
+        self.__address = ("", port)
+        self.__useIPv6 = useIPv6
+
+    def getSelector(self):
+        """
+        Public method to get a reference to the selector object.
+
+        @return reference to the selector object
+        @rtype selectors.BaseSelector
+        """
+        return self.__selector
+
+    #######################################################################
+    ## Methods for receiving requests and sending the results.
+    #######################################################################
+
+    def sendJson(self, category, reply, params, reqestUuid=""):
+        """
+        Public method to send a single refactoring command to the server.
+
+        @param category service category
+        @type EricRequestCategory
+        @param reply reply name to be sent
+        @type str
+        @param params dictionary of named parameters for the request
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+            (defaults to "", i.e. no UUID received)
+        @type str
+        """
+        if self.__connection is not None:
+            commandDict = {
+                "jsonrpc": "2.0",
+                "category": category,
+                "reply": reply,
+                "params": params,
+                "uuid": reqestUuid,
+            }
+            self.sendJsonCommand(commandDict, self.__connection)
+
+    def sendJsonCommand(self, jsonCommand, sock):
+        """
+        Public method to send a JSON encoded command/response via a given socket.
+
+        @param jsonCommand dictionary containing the command data or a JSON encoded
+            command string
+        @type dict or str
+        @param sock reference to the socket to send the data to
+        @type socket.socket
+        @return flag indicating a successful transmission
+        @rtype bool
+        """
+        if isinstance(jsonCommand, dict):
+            jsonCommand = json.dumps(jsonCommand)
+        print("Eric Server Send:", jsonCommand)
+
+        data = jsonCommand.encode("utf8", "backslashreplace")
+        header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF)
+        try:
+            sock.sendall(header)
+            sock.sendall(data)
+            return True
+        except BrokenPipeError:
+            return False
+
+    def __receiveBytes(self, length, sock):
+        """
+        Private method to receive the given length of bytes.
+
+        @param length bytes to receive
+        @type int
+        @param sock reference to the socket to receive the data from
+        @type socket.socket
+        @return received bytes or None if connection closed
+        @rtype bytes
+        """
+        data = bytearray()
+        while sock is not None and len(data) < length:
+            try:
+                newData = sock.recv(length - len(data))
+                if not newData:
+                    print(str(newData))
+                    return None
+
+                data += newData
+            except OSError as err:
+                if err.errno != 11:
+                    data = None  # in case some data was received already
+                    break
+        return data
+
+    def receiveJsonCommand(self, sock):
+        """
+        Public method to receive a JSON encoded command and data.
+
+        @param sock reference to the socket to receive the data from
+        @type socket.socket
+        @return dictionary containing the JSON command data or None to signal
+            an issue while receiving data
+        @rtype dict
+        """
+        if self.isSocketClosed(sock):
+            return None
+
+        header = self.__receiveBytes(struct.calcsize(b"!II"), sock)
+        if not header:
+            return {}
+
+        length, datahash = struct.unpack(b"!II", header)
+
+        length = int(length)
+        data = self.__receiveBytes(length, sock)
+        if data is None:
+            return None
+
+        if not data or zlib.adler32(data) & 0xFFFFFFFF != datahash:
+            self.sendJson(
+                category=EricRequestCategory.Error,
+                reply="EricServerChecksumException",
+                params={
+                    "ExceptionType": "ProtocolChecksumError",
+                    "ExceptionValue": "The checksum of the data does not match.",
+                    "ProtocolData": data.decode("utf8", "backslashreplace"),
+                },
+            )
+            return {}
+
+        jsonStr = data.decode("utf8", "backslashreplace")
+        print("Eric Server Receive:", jsonStr)
+        try:
+            return json.loads(jsonStr.strip())
+        except (TypeError, ValueError) as err:
+            self.sendJson(
+                category=EricRequestCategory.Error,
+                reply="EricServerException",
+                params={
+                    "ExceptionType": "ProtocolError",
+                    "ExceptionValue": str(err),
+                    "ProtocolData": jsonStr.strip(),
+                },
+            )
+            return {}
+
+    def __receiveJson(self):
+        """
+        Private method to receive a JSON encoded command and data from the
+        server.
+
+        @return tuple containing the received service category, the command,
+            a dictionary containing the associated data and the UUID of the
+            request
+        @rtype tuple of (int, str, dict, str)
+        """
+        requestDict = self.receiveJsonCommand(self.__connection)
+
+        if not requestDict:
+            return EricRequestCategory.Error, None, None, None
+
+        category = requestDict["category"]
+        request = requestDict["request"]
+        params = requestDict["params"]
+        reqestUuid = requestDict["uuid"]
+
+        return category, request, params, reqestUuid
+
+    def isSocketClosed(self, sock):
+        """
+        Public method to check, if a given socket is closed.
+
+        @param sock reference to the socket to be checked
+        @type socket.socket
+        @return flag indicating a closed state
+        @rtype bool
+        """
+        try:
+            # this will try to read bytes without removing them from buffer (peek only)
+            data = sock.recv(16, socket.MSG_PEEK)
+            if len(data) == 0:
+                return True
+        except BlockingIOError:
+            return False  # socket is open and reading from it would block
+        except ConnectionError:
+            return True  # socket was closed for some other reason
+        except Exception:
+            return False
+        return False
+
+    #######################################################################
+    ## Methods for the server main loop.
+    #######################################################################
+
+    def __initializeIdeSocket(self):
+        """
+        Private method to initialize and register the eric-ide server socket.
+        """
+        if socket.has_dualstack_ipv6() and self.__useIPv6:
+            self.__socket = socket.create_server(
+                self.__address, family=socket.AF_INET6, backlog=0, dualstack_ipv6=True
+            )
+        else:
+            self.__socket = socket.create_server(
+                self.__address, family=socket.AF_INET, backlog=0
+            )
+
+        self.__socket.listen(0)
+        self.__socket.setblocking(False)
+        print(f"Listening for 'eric-ide' connections on {self.__socket.getsockname()}")
+        data = types.SimpleNamespace(
+            name="server", acceptHandler=self.__acceptIdeConnection
+        )
+        self.__selector.register(self.__socket, selectors.EVENT_READ, data=data)
+
+    def __unregisterIdeSocket(self):
+        """
+        Private method to unregister the eric-ide server socket because only one
+        connection is allowed.
+        """
+        self.__selector.unregister(self.__socket)
+        self.__socket.shutdown(socket.SHUT_RDWR)
+        self.__socket.close()
+        self.__socket = None
+
+    def __shutdown(self):
+        """
+        Private method to shut down the server.
+        """
+        self.__closeIdeConnection(shutdown=True)
+
+        print("Stop listening for 'eric-ide' connections.")
+        if self.__socket is not None:
+            self.__socket.shutdown(socket.SHUT_RDWR)
+            self.__socket.close()
+
+        self.__selector.close()
+
+    def __acceptIdeConnection(self, sock):
+        """
+        Private method to accept the connection on the listening IDE server socket.
+
+        @param sock reference to the listening socket
+        @type socket.socket
+        """
+        connection, address = sock.accept()  # Should be ready to read
+        if self.__connection is None:
+            print(f"'eric-ide' connection from {address[0]},  port {address[1]}")
+            self.__connection = connection
+            self.__connection.setblocking(False)
+            data = types.SimpleNamespace(
+                name="eric-ide", address=address, handler=self.__serviceIdeConnection
+            )
+            events = selectors.EVENT_READ
+            self.__selector.register(self.__connection, events, data=data)
+
+            self.__unregisterIdeSocket()
+        else:
+            print(
+                f"'eric-ide' connection from {address[0]},  port {address[1]} rejected"
+            )
+            connection.close()
+
+    def __closeIdeConnection(self, shutdown=False):
+        """
+        Private method to close the connection to an eric-ide.
+
+        @param shutdown flag indicating a shutdown process
+        @type bool
+        """
+        if self.__connection is not None:
+            self.__selector.unregister(self.__connection)
+            try:
+                print(
+                    f"Closing 'eric-ide' connection to"
+                    f" {self.__connection.getpeername()}."
+                )
+                self.__connection.shutdown(socket.SHUT_RDWR)
+                self.__connection.close()
+            except OSError:
+                print("'eric-ide' connection gone.")
+            self.__connection = None
+
+            self.__debuggerRequestHandler.shutdownClients()
+
+        if not shutdown:
+            self.__initializeIdeSocket()
+
+    def __serviceIdeConnection(self, key):
+        """
+        Private method to service the eric-ide connection.
+
+        @param key reference to the SelectorKey object associated with the connection
+            to be serviced
+        @type selectors.SelectorKey
+        """
+        if key.data.name == "eric-ide":
+            category, request, params, reqestUuid = self.__receiveJson()
+            if category == EricRequestCategory.Error or request is None:
+                self.__closeIdeConnection()
+                return
+
+            if category == EricRequestCategory.Server and request.lower() == "shutdown":
+                self.__shouldStop = True
+                return
+
+            self.__handleRequest(category, request, params, reqestUuid)
+
+    def run(self):
+        """
+        Public method implementing the remote server main loop.
+
+        Exiting the inner loop, that receives and dispatches the requests, will
+        cause the server to stop and exit. The main loop handles these requests.
+        <ul>
+        <li>exit - exit the handler loop and wait for the next connection</li>
+        <li>shutdown - exit the handler loop and perform a clean shutdown</li>
+        </ul>
+
+        @return flag indicating a clean shutdown
+        @rtype bool
+        """
+        cleanExit = True
+        self.__shouldStop = False
+
+        # initialize the eric-ide server socket and listen for new connections
+        self.__initializeIdeSocket()
+
+        # initialize the debug client server socket
+        self.__debuggerRequestHandler.initServerSocket()
+
+        while True:
+            try:
+                events = self.__selector.select(timeout=None)
+                for key, _mask in events:
+                    if key.data.name == "server":
+                        # it is an event for a server socket
+                        key.data.acceptHandler(key.fileobj)
+                    else:
+                        key.data.handler(key)
+
+            except KeyboardInterrupt:
+                # intercept user pressing Ctrl+C
+                self.__shouldStop = True
+
+            except Exception:
+                exctype, excval, exctb = sys.exc_info()
+                tbinfofile = io.StringIO()
+                traceback.print_tb(exctb, None, tbinfofile)
+                tbinfofile.seek(0)
+                tbinfo = tbinfofile.read()
+
+                print(f"{str(exctype)} / {str(excval)} / {tbinfo}")
+
+                self.__shouldStop = True
+                cleanExit = False
+
+            if self.__shouldStop:
+                # exit the outer loop and shut down the server
+                self.__shutdown()
+                break
+
+        return cleanExit
+
+    #######################################################################
+    ## Methods for registering and unregistering handlers.
+    #######################################################################
+
+    def registerRequestHandler(self, requestCategory, handler):
+        """
+        Public method to register a request handler method for the given request
+        category.
+
+        @param requestCategory request category to be registered
+        @type EricRequestCategory or int (>= EricRequestCategory.UserCategory)
+        @param handler reference to the handler method. This handler must accept
+            the parameters 'request', 'params', and 'requestUuid'
+        @type function(request:str, params:dict, requestUuid:str)
+        @exception ValueError raised to signal a request category collision
+        """
+        if requestCategory in self.__requestCategoryHandlerRegistry:
+            raise ValueError(f"Request category '{requestCategory} already registered.")
+
+        self.__requestCategoryHandlerRegistry[requestCategory] = handler
+
+    def unregisterRequestHandler(self, requestCategory, ignoreError=False):
+        """
+        Public method to unregister a handler for the given request category.
+
+        Note: This method will raise a KeyError exception in case the request
+        category has not been registered and ignoreError is False (the default).
+
+        @param requestCategory request category to be unregistered
+        @type EricRequestCategory or int (>= EricRequestCategory.UserCategory)
+        @param ignoreError flag indicating to ignore errors (defaults to False)
+        @type bool (optional)
+        """
+        try:
+            del self.__requestCategoryHandlerRegistry[requestCategory]
+        except KeyError:
+            if not ignoreError:
+                raise
+
+    def __registerInternalHandlers(self):
+        """
+        Private method to register request handler categories of this class.
+        """
+        self.registerRequestHandler(EricRequestCategory.Echo, self.__handleEchoRequest)
+        self.registerRequestHandler(
+            EricRequestCategory.Server, self.__handleServerRequest
+        )
+        self.registerRequestHandler(EricRequestCategory.Error, None)
+        # Register a None handler to indicate we are not expecting a request of the
+        # 'Error' category.
+
+    #######################################################################
+    ## Request handler methods.
+    #######################################################################
+
+    def __handleRequest(self, category, request, params, reqestUuid):
+        """
+        Private method handling or dispatching the received requests.
+
+        @param category category of the request
+        @type EricRequestCategory
+        @param request request name
+        @type str
+        @param params request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+        @type str
+        """
+        try:
+            handler = self.__requestCategoryHandlerRegistry[category]
+            handler(request=request, params=params, reqestUuid=reqestUuid)
+        except KeyError:
+            self.sendJson(
+                category=EricRequestCategory.Error,
+                reply="UnsupportedServiceCategory",
+                params={"Category": category},
+            )
+
+    def __handleEchoRequest(self, request, params, reqestUuid):  # noqa: U100
+        """
+        Private method to handle an 'Echo' request.
+
+        @param request request name
+        @type str
+        @param params request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+            (defaults to "", i.e. no UUID received)
+        @type str
+        """
+        self.sendJson(
+            category=EricRequestCategory.Echo,
+            reply="Echo",
+            params=params,
+            reqestUuid=reqestUuid,
+        )
+
+    def __handleServerRequest(self, request, params, reqestUuid):  # noqa: U100
+        """
+        Private method to handle a 'Server' request.
+
+        @param request request name
+        @type str
+        @param params request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+            (defaults to "", i.e. no UUID received)
+        @type str
+        """
+        # 'Exit' and 'Shutdown' are handled in the 'run()' method.
+
+        if request.lower() == "versions":
+            self.sendJson(
+                category=EricRequestCategory.Server,
+                reply="Versions",
+                params={
+                    "python": sys.version.split()[0],
+                    "py_bitsize": "64-Bit" if sys.maxsize > 2**32 else "32-Bit",
+                    "version": Version,
+                    "hostname": socket.gethostname(),
+                },
+                reqestUuid=reqestUuid,
+            )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/EricServerCoverageRequestHandler.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the code coverage request handler of the eric-ide server.
+"""
+
+from coverage import Coverage
+from coverage.misc import CoverageException
+
+from eric7.SystemUtilities import FileSystemUtilities
+
+from .EricRequestCategory import EricRequestCategory
+
+
+class EricServerCoverageRequestHandler:
+    """
+    Class implementing the code coverage request handler of the eric-ide server.
+    """
+
+    def __init__(self, server):
+        """
+        Constructor
+
+        @param server reference to the eric-ide server object
+        @type EricServer
+        """
+        self.__server = server
+
+        self.__requestMethodMapping = {
+            "LoadData": self.__loadCoverageData,
+            "AnalyzeFile": self.__analyzeFile,
+            "AnalyzeFiles": self.__analyzeFiles,
+            "AnalyzeDirectory": self.__analyzeDirectory,
+        }
+
+        self.__cover = None
+
+    def handleRequest(self, request, params, reqestUuid):
+        """
+        Public method handling the received file system requests.
+
+        @param request request name
+        @type str
+        @param params dictionary containing the request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+        @type str
+        """
+        try:
+            result = self.__requestMethodMapping[request](params)
+            self.__server.sendJson(
+                category=EricRequestCategory.Coverage,
+                reply=request,
+                params=result,
+                reqestUuid=reqestUuid,
+            )
+
+        except KeyError:
+            self.__server.sendJson(
+                category=EricRequestCategory.Coverage,
+                reply=request,
+                params={"Error": f"Request type '{request}' is not supported."},
+            )
+
+    def __loadCoverageData(self, params):
+        """
+        Private method to load the data collected by a code coverage run.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        if self.__cover is not None:
+            del self.__cover
+            self.__cover = None
+
+        try:
+            self.__cover = Coverage(data_file=params["data_file"])
+            self.__cover.load()
+            if params["exclude"]:
+                self.__cover.exclude(params["exclude"])
+            return {"ok": True}
+        except CoverageException as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __analyzeFile(self, params):
+        """
+        Private method to analyze a single file.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        if self.__cover is None:
+            return {
+                "ok": False,
+                "error": "Coverage data has to be loaded first.",
+            }
+
+        try:
+            return {
+                "ok": True,
+                "result": self.__cover.analysis2(params["filename"]),
+            }
+        except CoverageException as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __analyzeFiles(self, params):
+        """
+        Private method to analyze a list of files.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        # TODO: not implemented yet
+        if self.__cover is None:
+            return {
+                "ok": False,
+                "error": "Coverage data has to be loaded first.",
+            }
+
+        try:
+            return {
+                "ok": True,
+                "results": [self.__cover.analysis2(f) for f in params["filenames"]],
+            }
+        except CoverageException as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __analyzeDirectory(self, params):
+        """
+        Private method to analyze files of a directory tree.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        # TODO: not implemented yet
+        if self.__cover is None:
+            return {
+                "ok": False,
+                "error": "Coverage data has to be loaded first.",
+            }
+
+        files = FileSystemUtilities.direntries(params["directory"], True, "*.py", False)
+
+        try:
+            return {
+                "ok": True,
+                "results": [self.__cover.analysis2(f) for f in files],
+            }
+        except CoverageException as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,334 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the debugger request handler of the eric-ide server.
+"""
+
+import json
+import os
+import selectors
+import socket
+import subprocess
+import sys
+import types
+
+from .EricRequestCategory import EricRequestCategory
+
+
+class EricServerDebuggerRequestHandler:
+    """
+    Class implementing the debugger request handler of the eric-ide server.
+    """
+
+    def __init__(self, server):
+        """
+        Constructor
+
+        @param server reference to the eric-ide server object
+        @type EricServer
+        """
+        self.__server = server
+
+        self.__requestMethodMapping = {
+            "StartClient": self.__startClient,
+            "StopClient": self.__stopClient,
+            "DebugClientCommand": self.__relayDebugClientCommand,
+        }
+
+        self.__mainClientId = None
+        self.__client = None
+        self.__inStartClient = False
+        self.__pendingConnections = []
+        self.__connections = {}
+
+        address = ("127.0.0.1", 0)
+        self.__socket = socket.create_server(address, family=socket.AF_INET)
+
+    def initServerSocket(self):
+        """
+        Public method to initialize the server socket listening for debug client
+        connections.
+        """
+        # listen on the debug server socket
+        self.__socket.listen()
+        self.__socket.setblocking(False)
+        print(
+            f"Listening for 'debug client' connections on"
+            f" {self.__socket.getsockname()}"
+        )
+        data = types.SimpleNamespace(
+            name="server", acceptHandler=self.__acceptDbgClientConnection
+        )
+        self.__server.getSelector().register(
+            self.__socket, selectors.EVENT_READ, data=data
+        )
+
+    def handleRequest(self, request, params, reqestUuid):
+        """
+        Public method handling the received debugger requests.
+
+        @param request request name
+        @type str
+        @param params dictionary containing the request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+        @type str
+        """
+        try:
+            result = self.__requestMethodMapping[request](params)
+            if result:
+                self.__server.sendJson(
+                    category=EricRequestCategory.Debugger,
+                    reply=request,
+                    params=result,
+                    reqestUuid=reqestUuid,
+                )
+
+        except KeyError:
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebuggerRequestError",
+                params={"Error": f"Request type '{request}' is not supported."},
+            )
+
+    #######################################################################
+    ## DebugServer like methods.
+    #######################################################################
+
+    def __acceptDbgClientConnection(self, sock):
+        """
+        Private method to accept the connection on the listening debug server socket.
+
+        @param sock reference to the listening socket
+        @type socket.socket
+        """
+        connection, address = sock.accept()  # Should be ready to read
+        print(f"'Debug client' connection from {address[0]},  port {address[1]}")
+        connection.setblocking(False)
+        self.__pendingConnections.append(connection)
+
+        data = types.SimpleNamespace(
+            name="debug_client",
+            address=address,
+            handler=self.__serviceDbgClientConnection,
+        )
+        self.__server.getSelector().register(
+            connection, selectors.EVENT_READ, data=data
+        )
+
+    def __serviceDbgClientConnection(self, key):
+        """
+        Private method to service the debug client connection.
+
+        @param key reference to the SelectorKey object associated with the connection
+            to be serviced
+        @type selectors.SelectorKey
+        """
+        sock = key.fileobj
+        data = self.__server.receiveJsonCommand(sock)
+
+        if data is None:
+            # socket was closed by debug client
+            self.__clientSocketDisconnected(sock)
+        elif data:
+            method = data["method"]
+
+            # 1. process debug client messages before relaying
+            if method == "DebuggerId" and sock in self.__pendingConnections:
+                debuggerId = data["params"]["debuggerId"]
+                self.__connections[debuggerId] = sock
+                self.__pendingConnections.remove(sock)
+                if self.__mainClientId is None:
+                    self.__mainClientId = debuggerId
+
+            elif method == "ResponseBanner":
+                # add an indicator for the eric-ide server
+                data["params"]["platform"] += " (eric-ide Server)"
+
+            # 2. pass on the data to the eric-ide
+            jsonStr = json.dumps(data)
+            print("Client Response:", jsonStr)
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebugClientResponse",
+                params={"response": jsonStr},
+            )
+
+            # 3. process debug client messages after relaying
+            if method == "ResponseExit":
+                for sock in list(self.__connections.values()):
+                    if not self.__server.isSocketClosed(sock):
+                        self.__clientSocketDisconnected(sock)
+
+    def __clientSocketDisconnected(self, sock):
+        """
+        Private method handling a socket disconnecting.
+
+        @param sock reference to the disconnected socket
+        @type socket.socket
+        """
+        self.__server.getSelector().unregister(sock)
+
+        for debuggerId in list(self.__connections):
+            if self.__connections[debuggerId] is sock:
+                del self.__connections[debuggerId]
+                self.__server.sendJson(
+                    category=EricRequestCategory.Debugger,
+                    reply="DebugClientDisconnected",
+                    params={"debugger_id": debuggerId},
+                )
+
+                if debuggerId == self.__mainClientId:
+                    self.__mainClientId = None
+
+                break
+        else:
+            if sock in self.__pendingConnections:
+                self.__pendingConnections.remove(sock)
+
+        sock.close()
+
+    def __mainClientExited(self):
+        """
+        Private method to handle exiting of the main debug client.
+        """
+        self.__server.sendJson(
+            category=EricRequestCategory.Debugger,
+            reply="MainClientExited",
+            params={"debugger_id": self.__mainClientId if self.__mainClientId else ""},
+        )
+
+    def shutdownClients(self):
+        """
+        Public method to shut down all connected clients.
+        """
+        if not self.__client:
+            # no client started yet
+            return
+
+        while self.__pendingConnections:
+            sock = self.__pendingConnections.pop()
+            commandDict = self.__prepareClientCommand("RequestShutdown", {})
+            self.__server.sendJsonCommand(commandDict, sock)
+            self.__shutdownSocket("", sock)
+
+        while self.__connections:
+            debuggerId, sock = self.__connections.popitem()
+            commandDict = self.__prepareClientCommand("RequestShutdown", {})
+            self.__server.sendJsonCommand(commandDict, sock)
+            self.__shutdownSocket(debuggerId, sock)
+
+        # reinitialize
+        self.__mainClientId = None
+        self.__client = None
+
+    def __shutdownSocket(self, debuggerId, sock):
+        """
+        Private method to shut down a socket.
+
+        @param debuggerId ID of the debugger the socket belongs to
+        @type str
+        @param sock reference to the socket
+        @type socket.socket
+        """
+        self.__server.getSelector().unregister(sock)
+        sock.shutdown(socket.SHUT_RDWR)
+        sock.close()
+
+        if debuggerId:
+            self.__server.sendJson(
+                category=EricRequestCategory.Debugger,
+                reply="DebugClientDisconnected",
+                params={"debugger_id": debuggerId},
+            )
+
+    def __prepareClientCommand(self, command, params):
+        """
+        Private method to prepare a command dictionary for the debug client.
+
+        @param command command to be sent
+        @type str
+        @param params dictionary containing the command parameters
+        @type dict
+        @return completed command dictionary to be sent to the debug client
+        @rtype dict
+        """
+        return {
+            "jsonrpc": "2.0",
+            "method": command,
+            "params": params,
+        }
+
+    #######################################################################
+    ## Individual request handler methods.
+    #######################################################################
+
+    def __startClient(self, params):
+        """
+        Private method to start a debug client process.
+
+        @param params dictionary containing the request data
+        @type dict
+        """
+        self.__inStartClient = True
+
+        # start a debug client
+        debugClient = os.path.abspath(
+            os.path.join(
+                os.path.dirname(__file__),
+                "..",
+                "DebugClients",
+                "Python",
+                "DebugClient.py",
+            )
+        )
+        ipaddr, port = self.__socket.getsockname()
+        args = [sys.executable, debugClient]
+        args.extend(params["arguments"])
+        args.extend([str(port), "True", ipaddr])
+
+        self.__client = subprocess.Popen(
+            args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+        )
+
+    def __stopClient(self, params):  # noqa: U100
+        """
+        Private method to stop the current debug client process.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        self.shutdownClients()
+
+        return {"ok": True}
+
+    def __relayDebugClientCommand(self, params):
+        """
+        Private method to relay a debug client command to the client.
+
+        @param params dictionary containing the request data
+        @type dict
+        """
+        debuggerId = params["debugger_id"]
+        jsonStr = params["command"]
+        print(debuggerId, "->", jsonStr)
+
+        if not debuggerId and self.__mainClientId and "RequestBanner" in jsonStr:
+            # modify the target for the 'RequestBanner' request
+            debuggerId = self.__mainClientId
+
+        if debuggerId == "<<all>>":
+            # broadcast to all connected debug clients
+            for sock in self.__connections.values():
+                self.__server.sendJsonCommand(jsonStr, sock)
+        else:
+            try:
+                sock = self.__connections[debuggerId]
+                self.__server.sendJsonCommand(jsonStr, sock)
+            except KeyError:
+                print(f"Command for unknown debugger ID '{debuggerId}' received.")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/EricServerFileSystemRequestHandler.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,402 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the file system request handler of the eric-ide server.
+"""
+
+import base64
+import contextlib
+import os
+import stat
+import time
+
+from eric7.SystemUtilities import FileSystemUtilities
+
+from .EricRequestCategory import EricRequestCategory
+
+
+class EricServerFileSystemRequestHandler:
+    """
+    Class implementing the file system request handler of the eric-ide server.
+    """
+
+    def __init__(self, server):
+        """
+        Constructor
+
+        @param server reference to the eric-ide server object
+        @type EricServer
+        """
+        self.__server = server
+
+        self.__requestMethodMapping = {
+            "GetPathSep": self.__getPathSeparator,
+            "Chdir": self.__chdir,
+            "Getcwd": self.__getcwd,
+            "Listdir": self.__listdir,
+            "Mkdir": self.__mkdir,
+            "MakeDirs": self.__makedirs,
+            "Rmdir": self.__rmdir,
+            "Replace": self.__replace,
+            "Remove": self.__remove,
+            "Stat": self.__stat,
+            "Exists": self.__exists,
+            "Access": self.__access,
+            "ReadFile": self.__readFile,
+            "WriteFile": self.__writeFile,
+            "DirEntries": self.__dirEntries,
+        }
+
+    def handleRequest(self, request, params, reqestUuid):
+        """
+        Public method handling the received file system requests.
+
+        @param request request name
+        @type str
+        @param params dictionary containing the request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+        @type str
+        """
+        try:
+            result = self.__requestMethodMapping[request](params)
+            self.__server.sendJson(
+                category=EricRequestCategory.FileSystem,
+                reply=request,
+                params=result,
+                reqestUuid=reqestUuid,
+            )
+
+        except KeyError:
+            self.__server.sendJson(
+                category=EricRequestCategory.FileSystem,
+                reply=request,
+                params={
+                    "ok": False,
+                    "error": f"Request type '{request}' is not supported.",
+                    "info": list(self.__requestMethodMapping.keys()),
+                },
+                reqestUuid=reqestUuid,
+            )
+
+    def __getPathSeparator(self, params):  # noqa: U100
+        """
+        Private method to report the path separator.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        return {"separator": os.sep}
+
+    def __chdir(self, params):
+        """
+        Private method to change the current working directory.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        try:
+            os.chdir(params["directory"])
+            return {"ok": True}
+        except OSError as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __getcwd(self, params):  # noqa: U100
+        """
+        Private method to report the current working directory.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        return {"directory": os.getcwd()}
+
+    def __listdir(self, params):
+        """
+        Private method to report a directory listing.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        directory = params["directory"]
+        if not directory:
+            directory = os.getcwd()
+
+        try:
+            listing = []
+            for dirEntry in os.scandir(directory):
+                filestat = dirEntry.stat()
+                entry = {
+                    "name": dirEntry.name,
+                    "path": dirEntry.path,
+                    "is_dir": dirEntry.is_dir(),
+                    "is_file": dirEntry.is_file(),
+                    "is_link": dirEntry.is_symlink(),
+                    "mode": filestat.st_mode,
+                    "mode_str": stat.filemode(filestat.st_mode),
+                    "size": filestat.st_size,
+                    "mtime": time.strftime(
+                        "%Y-%m-%d %H:%M:%S", time.localtime(filestat.st_mtime)
+                    ),
+                }
+                listing.append(entry)
+
+            return {
+                "ok": True,
+                "directory": directory,
+                "listing": listing,
+                "separator": os.sep,
+            }
+        except OSError as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __stat(self, params):
+        """
+        Private method to get the status of a file.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        try:
+            result = os.stat(params["filename"])
+            resultDict = {st: getattr(result, st) for st in params["st_names"]}
+            return {"ok": True, "result": resultDict}
+        except OSError as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __exists(self, params):
+        """
+        Private method to check if a file or directory of the given name exists.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        return {"exists": os.path.exists(params["name"])}
+
+    def __access(self, params):
+        """
+        Private method to test, if the eric-ide server has the given access rights
+        to a file or directory..
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        mode = os.F_OK
+        for modeStr in params["modes"]:
+            if modeStr == "read":
+                mode |= os.R_OK
+            elif modeStr == "write":
+                mode |= os.W_OK
+            elif modeStr == "execute":
+                mode |= os.X_OK
+
+        return {"ok": os.access(params["name"], mode)}
+
+    def __mkdir(self, params):
+        """
+        Private method to create a new directory.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        try:
+            os.mkdir(params["directory"])
+            return {"ok": True}
+        except OSError as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __makedirs(self, params):
+        """
+        Private method to create a new directory.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        try:
+            os.makedirs(params["directory"], exist_ok=params["exist_ok"])
+            return {"ok": True}
+        except OSError as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __rmdir(self, params):
+        """
+        Private method to delete a directory.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        try:
+            os.rmdir(params["directory"])
+            return {"ok": True}
+        except OSError as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __replace(self, params):
+        """
+        Private method to replace (rename) a file or directory.
+
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        try:
+            os.replace(params["old_name"], params["new_name"])
+            return {"ok": True}
+        except OSError as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __remove(self, params):
+        """
+        Private method to delete a file.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        try:
+            os.remove(params["filename"])
+            return {"ok": True}
+        except OSError as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __readFile(self, params):
+        """
+        Private method to read the contents of a file.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        filename = params["filename"]
+
+        if params["create"] and not os.path.exists(filename):
+            with open(filename, "wb"):
+                pass
+
+        try:
+            with open(filename, "rb") as f:
+                data = f.read()
+            return {
+                "ok": True,
+                "filedata": str(base64.b85encode(data), encoding="ascii"),
+            }
+        except OSError as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __writeFile(self, params):
+        """
+        Private method to write data into a file.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        filename = params["filename"]
+        data = base64.b85decode(bytes(params["filedata"], encoding="ascii"))
+
+        # 1. create backup file if asked for
+        if params["with_backup"]:
+            if os.path.islink(filename):
+                filename = os.path.realpath(filename)
+            backupFilename = "{0}~".format(filename)
+            try:
+                permissions = os.stat(filename).st_mode
+                perms_valid = True
+            except OSError:
+                # if there was an error, ignore it
+                perms_valid = False
+            with contextlib.suppress(OSError):
+                os.remove(backupFilename)
+            with contextlib.suppress(OSError):
+                os.rename(filename, backupFilename)
+
+        # 2. write the data to the file and reset the permissions
+        try:
+            with open(filename, "wb") as f:
+                f.write(data)
+            if params["with_backup"] and perms_valid:
+                os.chmod(filename, permissions)
+            return {"ok": True}
+        except OSError as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __dirEntries(self, params):
+        """
+        Private method to get a list of all files and directories of a given directory.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        directory = params["directory"]
+        result = FileSystemUtilities.direntries(
+            directory,
+            filesonly=params["files_only"],
+            pattern=params["pattern"],
+            followsymlinks=params["follow_symlinks"],
+            ignore=params["ignore"],
+            recursive=params["recursive"],
+            dirsonly=params["dirs_only"],
+        )
+        return {
+            "ok": True,
+            "result": result,
+        }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/__init__.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package implementing the components of the eric-ide remote server.
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerConnectionDialog.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+
+"""
+Module implementing a dialog to enter the parameters for a connection to an eric-ide
+server.
+"""
+
+import ipaddress
+
+from PyQt6.QtCore import pyqtSlot
+from PyQt6.QtWidgets import QDialog, QDialogButtonBox
+
+from eric7 import Preferences
+
+from .Ui_EricServerConnectionDialog import Ui_EricServerConnectionDialog
+
+
+class EricServerConnectionDialog(QDialog, Ui_EricServerConnectionDialog):
+    """
+    Class implementing a dialog to enter the parameters for a connection to an eric-ide
+    server.
+    """
+
+    def __init__(self, profileNames=None, parent=None):
+        """
+        Constructor
+
+        @param profileNames list of defined connection profile names (defaults to None)
+        @type list of str (optional)
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.timeoutSpinBox.setToolTip(
+            self.tr(
+                "Enter the timeout for the connection attempt (default: {0} s."
+            ).format(Preferences.getEricServer("ConnectionTimeout"))
+        )
+
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False)
+
+        if profileNames is None:
+            self.nameLabel.setVisible(False)
+            self.nameEdit.setVisible(False)
+            self.nameEdit.setEnabled(False)
+
+        self.__profileNames = profileNames[:] if bool(profileNames) else []
+        self.__originalName = ""
+
+        self.nameEdit.textChanged.connect(self.__updateOK)
+        self.hostnameEdit.textChanged.connect(self.__updateOK)
+
+        msh = self.minimumSizeHint()
+        self.resize(max(self.width(), msh.width()), msh.height())
+
+    @pyqtSlot()
+    def __updateOK(self):
+        """
+        Private slot to update the enabled state of the OK button.
+        """
+        hostname = self.hostnameEdit.text()
+
+        if hostname and hostname[0] in "0123456789" and ":" not in hostname:
+            # possibly an IPv4 address
+            try:
+                ipaddress.IPv4Address(hostname)
+                valid = True
+            except ipaddress.AddressValueError:
+                # leading zeros are not allowed
+                valid = False
+        elif ":" in hostname:
+            # possibly an IPv6 address
+            try:
+                ipaddress.IPv6Address(hostname)
+                valid = True
+            except ipaddress.AddressValueError:
+                # leading zeros are not allowed
+                valid = False
+        elif ":" not in hostname:
+            valid = bool(hostname)
+        else:
+            valid = False
+
+        if self.nameEdit.isEnabled():
+            # connection profile mode
+            name = self.nameEdit.text()
+            valid &= name == self.__originalName or name not in self.__profileNames
+
+        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(valid)
+
+    def getData(self):
+        """
+        Public method to get the entered data.
+
+        @return tuple containing the entered host name or IP address, the port number
+            and the timeout (in seconds)
+        @rtype tuple of (str, int, int)
+        """
+        port = self.portSpinBox.value()
+        if port == self.portSpinBox.minimum():
+            port = None
+
+        timeout = self.timeoutSpinBox.value()
+        if timeout == self.timeoutSpinBox.minimum():
+            timeout = None
+
+        return self.hostnameEdit.text(), port, timeout
+
+    def getProfileData(self):
+        """
+        Public method to get the entered data for connection profile mode.
+
+        @return tuple containing the profile name, host name or IP address,
+            the port number and the timeout (in seconds)
+        @rtype tuple of (str, str, int, int)
+        """
+        port = self.portSpinBox.value()
+        if port == self.portSpinBox.minimum():
+            port = 0
+
+        timeout = self.timeoutSpinBox.value()
+        if timeout == self.timeoutSpinBox.minimum():
+            timeout = 0
+
+        return self.nameEdit.text(), self.hostnameEdit.text(), port, timeout
+
+    def setProfileData(self, name, hostname, port, timeout):
+        """
+        Public method to set the connection profile data to be edited.
+
+        @param name profile name
+        @type str
+        @param hostname host name or IP address
+        @type str
+        @param port port number
+        @type int
+        @param timeout timeout value in seconds
+        @type int
+        """
+        # adjust some values
+        if not bool(port):
+            port = self.portSpinBox.minimum()
+        if not bool(timeout):
+            timeout = self.timeoutSpinBox.minimum()
+
+        self.__originalName = name
+
+        self.nameEdit.setText(name)
+        self.hostnameEdit.setText(hostname)
+        self.portSpinBox.setValue(port)
+        self.timeoutSpinBox.setValue(timeout)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerConnectionDialog.ui	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EricServerConnectionDialog</class>
+ <widget class="QDialog" name="EricServerConnectionDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>169</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>eric-ide Server Connection</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QLabel" name="nameLabel">
+     <property name="text">
+      <string>Name:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1" colspan="2">
+    <widget class="QLineEdit" name="nameEdit">
+     <property name="toolTip">
+      <string>Enter the name for the eric-ide server connection profile.</string>
+     </property>
+     <property name="clearButtonEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Hostname:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1" colspan="2">
+    <widget class="QLineEdit" name="hostnameEdit">
+     <property name="toolTip">
+      <string>Enter the hostname or IP address of the eric-ide server to connect to.</string>
+     </property>
+     <property name="clearButtonEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>Port:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="QSpinBox" name="portSpinBox">
+     <property name="toolTip">
+      <string>Enter the port number the eric-ide server listens on (default: 42024).</string>
+     </property>
+     <property name="wrapping">
+      <bool>true</bool>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+     </property>
+     <property name="specialValueText">
+      <string>default</string>
+     </property>
+     <property name="accelerated">
+      <bool>true</bool>
+     </property>
+     <property name="showGroupSeparator" stdset="0">
+      <bool>true</bool>
+     </property>
+     <property name="minimum">
+      <number>1024</number>
+     </property>
+     <property name="maximum">
+      <number>65535</number>
+     </property>
+     <property name="value">
+      <number>1024</number>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="2">
+    <spacer name="horizontalSpacer">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>240</width>
+       <height>20</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item row="3" column="0">
+    <widget class="QLabel" name="label_3">
+     <property name="text">
+      <string>Timeout:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="1">
+    <widget class="QSpinBox" name="timeoutSpinBox">
+     <property name="alignment">
+      <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+     </property>
+     <property name="specialValueText">
+      <string>default</string>
+     </property>
+     <property name="suffix">
+      <string> s</string>
+     </property>
+     <property name="maximum">
+      <number>60</number>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0" colspan="3">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>nameEdit</tabstop>
+  <tabstop>hostnameEdit</tabstop>
+  <tabstop>portSpinBox</tabstop>
+  <tabstop>timeoutSpinBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>EricServerConnectionDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>EricServerConnectionDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerCoverageInterface.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,250 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the code coverage interface to the eric-ide server.
+"""
+
+import contextlib
+
+from PyQt6.QtCore import QEventLoop, QObject
+
+from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
+from eric7.SystemUtilities import FileSystemUtilities
+
+
+class EricServerCoverageError(Exception):
+    """
+    Class defining a substitute exception for coverage errors of the server.
+    """
+
+    pass
+
+
+class EricServerCoverageInterface(QObject):
+    """
+    Class implementing the code coverage interface to the eric-ide server.
+    """
+
+    def __init__(self, serverInterface):
+        """
+        Constructor
+
+        @param serverInterface reference to the eric-ide server interface
+        @type EricServerInterface
+        """
+        super().__init__(parent=serverInterface)
+
+        self.__serverInterface = serverInterface
+
+    def loadCoverageData(self, dataFile, excludePattern=""):
+        """
+        Public method to tell the server to load the coverage data for a later analysis.
+
+        @param dataFile name of the data file to be loaded
+        @type str
+        @param excludePattern regular expression determining files to be excluded
+            (defaults to "")
+        @type str (optional)
+        @return tuple containing a success flag and an error message
+        @rtype tuple of (bool, str)
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error
+
+            if reply == "LoadData":
+                ok = params["ok"]
+                with contextlib.suppress(KeyError):
+                    error = params["error"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.Coverage,
+                request="LoadData",
+                params={
+                    "data_file": FileSystemUtilities.plainFileName(dataFile),
+                    "exclude": excludePattern,
+                },
+                callback=callback,
+            )
+
+            loop.exec()
+            return ok, error
+
+        else:
+            return False, "Not connected to an 'eric-ide' server."
+
+    def analyzeFile(self, filename):
+        """
+        Public method to analyze the code coverage of one file.
+
+        @param filename name of the file to be analyzed
+        @type str
+        @return list containing coverage result as reported by Coverage.analysis2()
+        @rtype list of [str, list of int, list of int, list of int, str]
+        @exception EricServerCoverageError raised to indicate a coverage exception
+        @exception OSError raised to indicate that server is not connected
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+        result = None
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error, result
+
+            if reply == "AnalyzeFile":
+                ok = params["ok"]
+                if ok:
+                    result = params["result"]
+                else:
+                    error = params["error"]
+                loop.quit()
+
+        if not self.__serverInterface.isServerConnected():
+            raise OSError("Not connected to an 'eric-ide' server.")
+
+        else:
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.Coverage,
+                request="AnalyzeFile",
+                params={"filename": FileSystemUtilities.plainFileName(filename)},
+                callback=callback,
+            )
+
+            loop.exec()
+            if not ok:
+                raise EricServerCoverageError(error)
+
+            return result
+
+    def analyzeFiles(self, filenames):
+        """
+        Public method to analyze the code coverage of a list of files.
+
+        @param filenames list of file names to be analyzed
+        @type str
+        @return lists containing coverage results as reported by Coverage.analysis2()
+        @rtype list of [list of [str, list of int, list of int, list of int, str]]
+        @exception EricServerCoverageError raised to indicate a coverage exception
+        @exception OSError raised to indicate that server is not connected
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+        result = None
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error, result
+
+            if reply == "AnalyzeFiles":
+                ok = params["ok"]
+                if ok:
+                    result = params["results"]
+                else:
+                    error = params["error"]
+                loop.quit()
+
+        if not self.__serverInterface.isServerConnected():
+            raise OSError("Not connected to an 'eric-ide' server.")
+
+        else:
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.Coverage,
+                request="AnalyzeFiles",
+                params={
+                    "filenames": [
+                        FileSystemUtilities.plainFileName(f) for f in filenames
+                    ]
+                },
+                callback=callback,
+            )
+
+            loop.exec()
+            if not ok:
+                raise EricServerCoverageError(error)
+
+            return result
+
+    def analyzeDirectory(self, directory):
+        """
+        Public method to analyze the code coverage of a directory.
+
+        @param directory directory name to be analyzed
+        @type str
+        @return lists containing coverage results as reported by Coverage.analysis2()
+        @rtype list of [list of [str, list of int, list of int, list of int, str]]
+        @exception EricServerCoverageError raised to indicate a coverage exception
+        @exception OSError raised to indicate that server is not connected
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+        result = None
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error, result
+
+            if reply == "AnalyzeDirectory":
+                ok = params["ok"]
+                if ok:
+                    result = params["results"]
+                else:
+                    error = params["error"]
+                loop.quit()
+
+        if not self.__serverInterface.isServerConnected():
+            raise OSError("Not connected to an 'eric-ide' server.")
+
+        else:
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.Coverage,
+                request="AnalyzeDirectory",
+                params={"directory": FileSystemUtilities.plainFileName(directory)},
+                callback=callback,
+            )
+
+            loop.exec()
+            if not ok:
+                raise EricServerCoverageError(error)
+
+            return result
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the file system interface to the eric-ide server.
+"""
+
+from PyQt6.QtCore import QEventLoop, QObject, pyqtSignal, pyqtSlot
+
+from eric7.EricWidgets import EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
+from eric7.SystemUtilities import FileSystemUtilities
+
+
+# TODO: sanitize all file names with FileSystemUtilities.plainFileName()
+class EricServerDebuggerInterface(QObject):
+    """
+    Class implementing the file system interface to the eric-ide server.
+
+    @signal debugClientResponse(response:str) emitted to relay a response of
+        the remote debug client
+    @signal debugClientDisconnected(debuggerId:str) emitted when a remote debug
+        client did disconnect from the eric-ide server
+    @signal lastClientExited() emitted to indicate that the last debug client of
+        the eric-ide server exited
+    """
+
+    debugClientResponse = pyqtSignal(str)
+    debugClientDisconnected = pyqtSignal(str)
+    lastClientExited = pyqtSignal()
+
+    def __init__(self, serverInterface):
+        """
+        Constructor
+
+        @param serverInterface reference to the eric-ide server interface
+        @type EricServerInterface
+        """
+        super().__init__(parent=serverInterface)
+
+        self.__serverInterface = serverInterface
+        self.__clientStarted = False
+
+        self.__replyMethodMapping = {
+            "DebuggerRequestError": self.__handleDbgRequestError,
+            "DebugClientResponse": self.__handleDbgClientResponse,
+            "DebugClientDisconnected": self.__handleDbgClientDisconnected,
+            "LastDebugClientExited": self.__handleLastDbgClientExited,
+            "MainClientExited": self.__handleMainClientExited,
+        }
+
+        # connect some signals
+        self.__serverInterface.remoteDebuggerReply.connect(self.__handleDebuggerReply)
+
+    def sendClientCommand(self, debuggerId, jsonCommand):
+        """
+        Public method to rely a debug client command via the eric-ide server.
+
+        @param debuggerId id of the debug client to send the command to
+        @type str
+        @param jsonCommand JSON encoded command dictionary to be relayed
+        @type str
+        """
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.Debugger,
+                request="DebugClientCommand",
+                params={"debugger_id": debuggerId, "command": jsonCommand},
+            )
+
+    @pyqtSlot(str, dict)
+    def __handleDebuggerReply(self, reply, params):
+        """
+        Private slot to handle a debugger reply from the eric-ide server.
+
+        @param reply name of the server reply
+        @type str
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        if self.__serverInterface.isServerConnected():
+            try:
+                self.__replyMethodMapping[reply](params)
+            except KeyError:
+                EricMessageBox.critical(
+                    None,
+                    self.tr("Unknown Server Reply"),
+                    self.tr(
+                        "<p>The eric-ide server debugger interface sent the unknown"
+                        " reply <b>{0}</b>.</p>"
+                    ).format(reply),
+                )
+
+    #######################################################################
+    ## Methods for handling of debug client replies.
+    #######################################################################
+
+    def __handleDbgRequestError(self, params):
+        """
+        Private method to handle an error reported by the debugger interface of
+        the eric-ide server.
+
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        EricMessageBox.warning(
+            None,
+            self.tr("Debug Client Command"),
+            self.tr(
+                "<p>The IDE received an error message.</p><p>Error: {0}</p>"
+            ).format(params["Error"]),
+        )
+
+    def __handleDbgClientResponse(self, params):
+        """
+        Private method to handle a response from a debug client connected to the
+        eric-ide server.
+
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        self.debugClientResponse.emit(params["response"])
+
+    def __handleDbgClientDisconnected(self, params):
+        """
+        Private method to handle a debug client disconnect report of the
+        eric-ide server.
+
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        self.debugClientDisconnected.emit(params["debugger_id"])
+
+    def __handleLastDbgClientExited(self, params):  # noqa: U100
+        """
+        Private method to handle a report of the eric-ide server, that the last
+        debug client has disconnected.
+
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        self.__clientStarted = False
+        self.lastClientExited.emit()
+
+    def __handleMainClientExited(self, params):  # noqa: U100
+        """
+        Private method to handle the main client exiting.
+
+        @param params dictionary containing the reply data
+        @type dict
+        """
+        self.__clientStarted = False
+        ericApp().getObject("DebugServer").signalMainClientExit()
+
+    #######################################################################
+    ## Methods for sending debug server commands to the eric-ide server.
+    #######################################################################
+
+    def startClient(self, interpreter, originalPathString, args, workingDir=""):
+        """
+        Public method to send a command to start a debug client.
+
+        @param interpreter path of the remote interpreter to be used
+        @type str
+        @param originalPathString original PATH environment variable
+        @type str
+        @param args list of command line parameters for the debug client
+        @type list of str
+        @param workingDir directory to start the debugger client in (defaults to "")
+        @type str (optional)
+        """
+        self.__serverInterface.sendJson(
+            category=EricRequestCategory.Debugger,
+            request="StartClient",
+            params={
+                "interpreter": FileSystemUtilities.plainFileName(interpreter),
+                "path": originalPathString,
+                "arguments": args,
+                "working_dir": FileSystemUtilities.plainFileName(workingDir),
+            },
+        )
+        self.__clientStarted = True
+
+    def stopClient(self):
+        """
+        Public method to stop the debug client synchronously.
+        """
+        if self.__serverInterface.isServerConnected() and self.__clientStarted:
+            loop = QEventLoop()
+
+            def callback(reply, params):  # noqa: U100
+                """
+                Function to handle the server reply
+
+                @param reply name of the server reply
+                @type str
+                @param params dictionary containing the reply data
+                @type dict
+                """
+                if reply == "StopClient":
+                    loop.quit()
+
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.Debugger,
+                request="StopClient",
+                params={},
+                callback=callback,
+            )
+
+            loop.exec()
+            self.__clientStarted = False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerFileDialog.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,1021 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a file dialog showing the file system of the eric-ide server.
+"""
+
+import enum
+import fnmatch
+import re
+
+from PyQt6.QtCore import QLocale, QPoint, Qt, pyqtSlot
+from PyQt6.QtWidgets import (
+    QAbstractItemView,
+    QCompleter,
+    QDialog,
+    QInputDialog,
+    QLineEdit,
+    QMenu,
+    QTreeWidgetItem,
+)
+
+from eric7.EricGui import EricPixmapCache
+from eric7.EricGui.EricFileIconProvider import EricFileIconProvider
+from eric7.EricWidgets import EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.Globals import dataString
+from eric7.SystemUtilities import FileSystemUtilities
+
+from .Ui_EricServerFileDialog import Ui_EricServerFileDialog
+
+
+class AcceptMode(enum.Enum):
+    """
+    Class defining the dialog accept modes.
+    """
+
+    AcceptOpen = 0
+    AcceptSave = 1
+
+
+class FileMode(enum.Enum):
+    """
+    Class defining what the user may select in the file dialog.
+    """
+
+    AnyFile = 0
+    ExistingFile = 1
+    Directory = 2
+    ExistingFiles = 3
+
+
+class EricServerFileDialog(QDialog, Ui_EricServerFileDialog):
+    """
+    Class implementing a file dialog showing the file system of the eric-ide server.
+    """
+
+    IsDirectoryRole = Qt.ItemDataRole.UserRole
+
+    def __init__(self, parent=None, caption="", directory="", filter=""):  # noqa: M132
+        """
+        Constructor
+
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        @param caption dialog title (defaults to "")
+        @type str (optional)
+        @param directory initial directory (defaults to "")
+        @type str (optional)
+        @param filter Qt file filter string (defaults to "")
+        @type str (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        # finish UI setup
+        self.backButton.setIcon(EricPixmapCache.getIcon("1leftarrow"))
+        self.forwardButton.setIcon(EricPixmapCache.getIcon("1rightarrow"))
+        self.upButton.setIcon(EricPixmapCache.getIcon("1uparrow"))
+        self.newDirButton.setIcon(EricPixmapCache.getIcon("dirNew"))
+        self.reloadButton.setIcon(EricPixmapCache.getIcon("reload"))
+        self.cancelButton.setIcon(EricPixmapCache.getIcon("dialog-cancel"))
+
+        self.setWindowTitle(caption)
+
+        self.__iconProvider = EricFileIconProvider()
+
+        self.__nameCompleter = QCompleter()
+        self.__nameCompleter.setModel(self.listing.model())
+        self.__nameCompleter.setCompletionColumn(0)
+        self.__nameCompleter.activated.connect(self.__nameCompleterActivated)
+        self.nameEdit.setCompleter(self.__nameCompleter)
+
+        self.__contextMenu = QMenu(self)
+
+        self.__fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
+        # set some default values
+        self.__fileMode = FileMode.ExistingFile
+        self.__dirsOnly = False
+        self.__acceptMode = AcceptMode.AcceptOpen
+        self.__showHidden = False
+        self.__sep = "/"
+        self.__filters = []
+
+        self.__history = []
+        self.__currentHistoryIndex = -1  # empty history
+        self.__updateHistoryButtons()
+
+        self.__filenameCache = []
+        self.__directoryCache = []
+        self.__selectedDirectory = None
+
+        self.setNameFilters(filter.split(";;"))
+
+        self.reloadButton.clicked.connect(self.__reload)
+        self.cancelButton.clicked.connect(self.reject)
+
+        self.treeCombo.currentTextChanged.connect(self.setDirectory)
+
+        self.setDirectory(FileSystemUtilities.plainFileName(directory))
+
+    def acceptMode(self):
+        """
+        Public method to get the accept mode of the dialog.
+
+        @return accept mode
+        @rtype AcceptMode
+        """
+        return self.__acceptMode
+
+    def setAcceptMode(self, mode):
+        """
+        Public method to set the accept mode of the dialog.
+
+        @param mode accept mode
+        @type AcceptMode
+        """
+        self.__acceptMode = mode
+
+        self.__updateOkButton()
+
+    def fileMode(self):
+        """
+        Public method to get the current file mode of the dialog.
+
+        @return file mode
+        @rtype FileMode
+        """
+        return self.__fileMode
+
+    def setFileMode(self, mode):
+        """
+        Public method to set the file mode of the dialog.
+
+        @param mode file mode
+        @type FileMode
+        """
+        self.__fileMode = mode
+
+        self.listing.clearSelection()
+        if mode == FileMode.ExistingFiles:
+            self.listing.setSelectionMode(
+                QAbstractItemView.SelectionMode.ExtendedSelection
+            )
+        else:
+            self.listing.setSelectionMode(
+                QAbstractItemView.SelectionMode.SingleSelection
+            )
+
+        if mode == FileMode.Directory:
+            self.setNameFilters([self.tr("Directories")])
+
+        self.__updateOkButton()
+
+    def setNameFilters(self, filters):
+        """
+        Public method to set the list of file/directory name filters.
+
+        @param filters list of filter expressions
+            ("filter_name (pattern1 ... patternN)")
+        @type list of str
+        """
+        self.__filters = []
+        for f in filters:
+            if " (" in f and ")" in f:
+                self.__filters.append(f.split(" (", 1)[1].split(")", 1)[0].split())
+            elif f:
+                self.__filters.append(f)
+
+        self.filterCombo.clear()
+        self.filterCombo.addItems([f for f in filters if f])
+
+    def setNameFilter(self, filter):  # noqa: M132
+        """
+        Public method to set the current name filter.
+
+        @param filter filter text to make current
+        @type str
+        """
+        self.filterCombo.setCurrentText(filter)
+
+    def setDirectoriesOnly(self, dirsOnly):
+        """
+        Public method to set a flag to just show directories.
+
+        @param dirsOnly flag indicating to just show directories
+        @type bool
+        """
+        self.__dirsOnly = dirsOnly
+
+        filters = self.__filters[self.filterCombo.currentIndex()]
+        self.__filterList(filters)
+
+    def __addToHistory(self, entry):
+        """
+        Private method to add a directory to the history list.
+
+        @param entry name of the directory to be added
+        @type str
+        """
+        try:
+            # is in the history already?
+            index = self.__history.index(entry)
+            self.__currentHistoryIndex = index
+        except ValueError:
+            # new entry
+            self.__history.append(entry)
+            self.__currentHistoryIndex = len(self.__history) - 1
+
+        self.__updateHistoryButtons()
+
+    @pyqtSlot()
+    def __updateHistoryButtons(self):
+        """
+        Private method to update the enabled state of the back and forward buttons.
+        """
+        if not self.__history:
+            self.backButton.setEnabled(False)
+            self.forwardButton.setEnabled(False)
+        else:
+            self.backButton.setEnabled(self.__currentHistoryIndex > 0)
+            self.forwardButton.setEnabled(
+                self.__currentHistoryIndex < len(self.__history) - 1
+            )
+
+    @pyqtSlot()
+    def on_backButton_clicked(self):
+        """
+        Private slot to move back in history of visited directories.
+        """
+        self.setDirectory(self.__history[self.__currentHistoryIndex - 1])
+
+    @pyqtSlot()
+    def on_forwardButton_clicked(self):
+        """
+        Private slot to move forward in history of visited directories.
+        """
+        self.setDirectory(self.__history[self.__currentHistoryIndex + 1])
+
+    @pyqtSlot()
+    def __updateUpButton(self):
+        """
+        Private slot to update the enabled state of the 'Up' button.
+        """
+        self.upButton.setEnabled(
+            self.treeCombo.currentIndex() < self.treeCombo.count() - 1
+        )
+
+    @pyqtSlot()
+    def on_upButton_clicked(self):
+        """
+        Private slot to move up one level in the hierarchy.
+        """
+        self.treeCombo.setCurrentIndex(self.treeCombo.currentIndex() + 1)
+
+    @pyqtSlot()
+    def on_newDirButton_clicked(self):
+        """
+        Private slot to create a new directory.
+        """
+        newDir, ok = QInputDialog.getText(
+            self,
+            self.tr("New Directory"),
+            self.tr("Enter the name for the new directory:"),
+            QLineEdit.EchoMode.Normal,
+        )
+        if ok and newDir:
+            if newDir in self.__directoryCache or newDir in self.__filenameCache:
+                EricMessageBox.warning(
+                    self,
+                    self.tr("New Directory"),
+                    self.tr(
+                        "<p>A file or directory with the name <b>{0}</b> exists"
+                        " already. Aborting...</p>"
+                    ).format(newDir),
+                )
+                return
+
+            ok, error = self.__fsInterface.mkdir(self.__getFullPath(newDir))
+            if ok:
+                # refresh
+                self.__reload()
+            else:
+                EricMessageBox.critical(
+                    self,
+                    self.tr("New Directory"),
+                    self.tr(
+                        "<p>The directory <b>{0}</b> could not be created.</p>"
+                        "<p>Reason: {1}</p>"
+                    ).format(
+                        self.__getFullPath(newDir),
+                        error if error else self.tr("Unknown"),
+                    ),
+                )
+
+    @pyqtSlot()
+    def __reload(self):
+        """
+        Private slot to reload the directory listing.
+        """
+        self.setDirectory(self.treeCombo.currentText())
+
+    @pyqtSlot(QTreeWidgetItem, int)
+    def on_listing_itemActivated(self, item, column):
+        """
+        Private slot to handle the activation of an item in the list.
+
+        @param item reference to the activated item
+        @type QTreeWidgetItem
+        @param column column number (unused)
+        @type int
+        """
+        if item.data(0, EricServerFileDialog.IsDirectoryRole):
+            self.setDirectory(self.__getFullPath(item.text(0)))
+        else:
+            self.accept()
+
+    @pyqtSlot()
+    def on_listing_itemSelectionChanged(self):
+        """
+        Private slot to handle the selection of listed items.
+        """
+        for itm in self.listing.selectedItems():
+            if itm.data(0, EricServerFileDialog.IsDirectoryRole):
+                self.__selectedDirectory = itm.text(0)
+                break
+        else:
+            self.__selectedDirectory = None
+
+        selected = []
+        for itm in self.listing.selectedItems():
+            isDir = itm.data(0, EricServerFileDialog.IsDirectoryRole)
+            if self.__fileMode == FileMode.Directory and isDir:
+                selected.append(itm.text(0))
+            elif not isDir:
+                selected.append(itm.text(0))
+
+        if len(selected) == 1:
+            self.nameEdit.setText(selected[0])
+        elif len(selected) > 1:
+            self.nameEdit.setText('"{0}"'.format('" "'.join(selected)))
+
+        self.__updateOkButton()
+
+    @pyqtSlot()
+    def __nameCompleterActivated(self):
+        """
+        Private slot handling the activation of the completer.
+        """
+        if self.okButton.isEnabled():
+            self.okButton.animateClick()
+
+    @pyqtSlot(str)
+    def on_nameEdit_textChanged(self, name):
+        """
+        Private slot handling the editing of a file or directory name.
+
+        @param name current text of the name edit
+        @type str
+        """
+        self.listing.clearSelection()
+        items = self.listing.findItems(name, Qt.MatchFlag.MatchExactly)
+        for itm in items:
+            itm.setSelected(True)
+
+        self.__updateOkButton()
+
+    def __getNames(self):
+        """
+        Private method to get the selected names list.
+
+        @return list containing the selected names
+        @rtype list of str
+        """
+        namesStr = self.nameEdit.text()
+        if namesStr.startswith('"'):
+            namesStr = namesStr[1:]
+        if namesStr.endswith('"'):
+            namesStr = namesStr[:-1]
+        names = re.split(r'"\s+"', namesStr)
+        return names
+
+    def __getFullPath(self, name):
+        """
+        Private method to get the full path for a given file or directory name.
+
+        @param name name of the file or directory
+        @type str
+        @return full path of the file or directory
+        @rtype str
+        """
+        return "{0}{1}{2}".format(self.treeCombo.currentText(), self.__sep, name)
+
+    @pyqtSlot()
+    def __updateOkButton(self):
+        """
+        Private slot to set the 'OK' button state, icon and label.
+        """
+        # 1. adjust icon and label
+        if (
+            self.__acceptMode == AcceptMode.AcceptOpen
+            or self.__selectedDirectory is not None
+        ):
+            self.okButton.setIcon(EricPixmapCache.getIcon("dialog-ok"))
+            if self.__fileMode != FileMode.Directory:
+                self.okButton.setText(self.tr("Open"))
+            else:
+                self.okButton.setText(self.tr("Choose"))
+        else:
+            self.okButton.setIcon(EricPixmapCache.getIcon("fileSave"))
+            self.okButton.setText(self.tr("Save"))
+
+        # 2. adjust enabled state
+        if self.__selectedDirectory and self.__fileMode != FileMode.Directory:
+            self.okButton.setEnabled(True)
+        elif self.__fileMode == FileMode.AnyFile:
+            self.okButton.setEnabled(bool(self.nameEdit.text()))
+        elif self.__fileMode == FileMode.ExistingFile:
+            self.okButton.setEnabled(self.nameEdit.text() in self.__filenameCache)
+        elif self.__fileMode == FileMode.ExistingFiles:
+            names = self.__getNames()
+            self.okButton.setEnabled(all(n in self.__filenameCache for n in names))
+        elif self.__fileMode == FileMode.Directory:
+            self.okButton.setEnabled(True)
+        else:
+            self.okButton.setEnabled(False)
+
+    @pyqtSlot()
+    def on_okButton_clicked(self):
+        """
+        Private slot handling the press of the OK button.
+        """
+        if self.__selectedDirectory and self.__fileMode != FileMode.Directory:
+            self.setDirectory(self.__getFullPath(self.__selectedDirectory))
+        else:
+            self.accept()
+
+    @pyqtSlot(int)
+    def on_filterCombo_currentIndexChanged(self, index):
+        """
+        Private slot handling the selection of a new file filter..
+
+        @param index index of the selected entry
+        @type int
+        """
+        filters = self.__filters[index]
+        self.__filterList(filters)
+
+    @pyqtSlot(str)
+    def setDirectory(self, directory):
+        """
+        Public slot to set the current directory and populate the tree list.
+
+        @param directory directory to be set as current. An empty directory sets the
+            server's current directory.
+        @type str
+        """
+        self.__filenameCache.clear()
+        self.__directoryCache.clear()
+
+        try:
+            directory, sep, dirListing = self.__fsInterface.listdir(directory)
+
+            self.__sep = sep
+
+            # 1. populate the directory tree combo box
+            self.treeCombo.blockSignals(True)
+            self.treeCombo.clear()
+            if len(directory) > 1 and directory.endswith(sep):
+                directory = directory[:-1]
+            if len(directory) > 2 and directory[1] == ":":
+                # starts with a Windows drive letter
+                directory = directory[2:]
+            directoryParts = directory.split(sep)
+            while directoryParts:
+                if directoryParts[-1]:
+                    self.treeCombo.addItem(sep.join(directoryParts))
+                directoryParts.pop()
+            self.treeCombo.addItem(sep)
+            self.treeCombo.blockSignals(False)
+
+            # 2. populate the directory listing
+            self.listing.clear()
+            for dirEntry in sorted(
+                dirListing,
+                key=lambda d: (
+                    " " + d["name"].lower() if d["is_dir"] else d["name"].lower()
+                ),
+            ):
+                if dirEntry["is_dir"]:
+                    type_ = self.tr("Directory")
+                    iconName = "dirClosed"
+                    sizeStr = ""
+                    self.__directoryCache.append(dirEntry["name"])
+                else:
+                    type_ = self.tr("File")
+                    iconName = self.__iconProvider.fileIconName(dirEntry["name"])
+                    sizeStr = dataString(dirEntry["size"], QLocale.system())
+                    self.__filenameCache.append(dirEntry["name"])
+                itm = QTreeWidgetItem(
+                    self.listing, [dirEntry["name"], sizeStr, type_, dirEntry["mtime"]]
+                )
+                itm.setIcon(0, EricPixmapCache.getIcon(iconName))
+                itm.setTextAlignment(1, Qt.AlignmentFlag.AlignRight)
+                itm.setTextAlignment(2, Qt.AlignmentFlag.AlignHCenter)
+                itm.setData(0, EricServerFileDialog.IsDirectoryRole, dirEntry["is_dir"])
+
+            currentFilterIndex = self.filterCombo.currentIndex()
+            filters = (
+                [] if currentFilterIndex == -1 else self.__filters[currentFilterIndex]
+            )
+            self.__filterList(filters)
+
+            # 3. add the directory to the history
+            self.__addToHistory(directory)
+
+        except OSError as err:
+            EricMessageBox.critical(
+                self,
+                self.tr("Remote Directory Listung"),
+                self.tr(
+                    "<p>The directory <b>{0}</b> could not be listed due to an error"
+                    " reported by the eric-ide server.</p><p>Reason: {1}</p>"
+                ).format(directory, str(err)),
+            )
+
+        # 4. update some dependent states
+        self.nameEdit.clear()
+        self.__updateUpButton()
+
+    @pyqtSlot(QPoint)
+    def on_listing_customContextMenuRequested(self, pos):
+        """
+        Private slot to show a context menu.
+
+        @param pos mouse pointer position to show the menu at
+        @type QPoint
+        """
+        self.__contextMenu.clear()
+
+        itm = self.listing.itemAt(pos)
+        if itm is not None:
+            self.__contextMenu.addAction(
+                self.tr("Rename"), lambda: self.__renameItem(itm)
+            )
+            self.__contextMenu.addAction(
+                self.tr("Delete"), lambda: self.__deleteItem(itm)
+            )
+            self.__contextMenu.addSeparator()
+        act = self.__contextMenu.addAction(self.tr("Show Hidden Files"))
+        act.setCheckable(True)
+        act.setChecked(self.__showHidden)
+        act.toggled.connect(self.__showHiddenToggled)
+        self.__contextMenu.addAction(
+            self.tr("New Directory"), self.on_newDirButton_clicked
+        )
+
+        self.__contextMenu.popup(self.listing.mapToGlobal(pos))
+
+    @pyqtSlot(QTreeWidgetItem)
+    def __renameItem(self, item):
+        """
+        Private slot to rename the given file/directory item.
+
+        @param item reference to the item to be renamed
+        @type QTreeWidgetItem
+        """
+        title = (
+            self.tr("Rename Directory")
+            if item.data(0, EricServerFileDialog.IsDirectoryRole)
+            else self.tr("Rename File")
+        )
+
+        newName, ok = QInputDialog.getText(
+            self,
+            title,
+            self.tr("<p>Enter the new name <b>{0}</b>:</p>").format(item.text(0)),
+            QLineEdit.EchoMode.Normal,
+            item.text(0),
+        )
+        if ok and newName:
+            if newName in self.__directoryCache or newName in self.__filenameCache:
+                EricMessageBox.warning(
+                    self,
+                    title,
+                    self.tr(
+                        "<p>A file or directory with the name <b>{0}</b> exists"
+                        " already. Aborting...</p>"
+                    ).format(newName),
+                )
+                return
+
+            ok, error = self.__fsInterface.replace(
+                self.__getFullPath(item.text(0)), self.__getFullPath(newName)
+            )
+            if ok:
+                # refresh
+                self.__reload()
+            else:
+                EricMessageBox.critical(
+                    self,
+                    title,
+                    self.tr(
+                        "<p>The renaming operation failed.</p><p>Reason: {0}</p>"
+                    ).format(error if error else self.tr("Unknown")),
+                )
+
+    @pyqtSlot(QTreeWidgetItem)
+    def __deleteItem(self, item):
+        """
+        Private slot to delete the given file/directory  item.
+
+        @param item reference to the item to be deleted
+        @type QTreeWidgetItem
+        """
+        isDir = item.data(0, EricServerFileDialog.IsDirectoryRole)
+        if isDir:
+            title = self.tr("Delete Directory")
+            itemType = self.tr("directory")
+        else:
+            title = self.tr("Delete File")
+            itemType = self.tr("file")
+
+        yes = EricMessageBox.yesNo(
+            self,
+            title,
+            self.tr("Shall the selected {0} really be deleted?").format(itemType),
+        )
+        if yes:
+            ok, error = (
+                self.__fsInterface.rmdir(self.__getFullPath(item.text(0)))
+                if isDir
+                else self.__fsInterface.remove(self.__getFullPath(item.text(0)))
+            )
+            if ok:
+                # refresh
+                self.__reload()
+            else:
+                EricMessageBox.critical(
+                    self,
+                    title,
+                    self.tr(
+                        "<p>The deletion operation failed.</p><p>Reason: {0}</p>"
+                    ).format(error if error else self.tr("Unknown")),
+                )
+
+    @pyqtSlot(bool)
+    def __showHiddenToggled(self, on):
+        """
+        Private slot to handle toggling the display of hidden files/directories.
+
+        @param on flag indicating to show hidden files and directories
+        @type bool
+        """
+        self.__showHidden = on
+        filters = self.__filters[self.filterCombo.currentIndex()]
+        self.__filterList(filters)
+
+    def selectedFiles(self):
+        """
+        Public method to get the selected files or the current viewport path.
+
+        @return selected files or current viewport path
+        @rtype str
+        """
+        if self.__fileMode == FileMode.Directory and not self.nameEdit.text():
+            return [self.treeCombo.currentText()]
+        else:
+            return [self.__getFullPath(n) for n in self.__getNames()]
+
+    def selectedNameFilter(self):
+        """
+        Public method to get the selected name filter.
+
+        @return selected name filter
+        @rtype str
+        """
+        return self.filterCombo.currentText()
+
+    def __isHidden(self, name):
+        """
+        Private method to check, if the given name is indicating a hidden file or
+        directory.
+
+        @param name name of the file or directory
+        @type str
+        @return flag indicating a hidden file or directory
+        @rtype bool
+        """
+        return name.startswith(".") or name.endswith("~")
+
+    def __filterList(self, filters):
+        """
+        Private method to filter the files and directories list based on the given
+        filters and whether hidden files/directories should be shown.
+
+        @param filters list of filter patterns (only applied to files
+        @type list of str
+        """
+        self.listing.clearSelection()
+        for row in range(self.listing.topLevelItemCount()):
+            itm = self.listing.topLevelItem(row)
+            name = itm.text(0)
+            if self.__dirsOnly and not itm.data(
+                0, EricServerFileDialog.IsDirectoryRole
+            ):
+                itm.setHidden(True)
+            elif not self.__showHidden and self.__isHidden(name):
+                # applies to files and directories
+                itm.setHidden(True)
+            elif not itm.data(0, EricServerFileDialog.IsDirectoryRole):
+                # it is a file item, apply the filter
+                itm.setHidden(not any(fnmatch.fnmatch(name, pat) for pat in filters))
+            else:
+                itm.setHidden(False)
+
+        # resize the columns
+        for column in range(4):
+            self.listing.resizeColumnToContents(column)
+
+
+###########################################################################
+## Module functions mimicing the interface of EricFileDialog/QFileDialog
+###########################################################################
+
+
+def getOpenFileName(
+    parent=None,
+    caption="",
+    directory="",
+    filterStr="",
+    initialFilter="",
+    withRemote=True,
+):
+    """
+    Module function to get the name of a file for opening it.
+
+    @param parent parent widget of the dialog (defaults to None)
+    @type QWidget (optional)
+    @param caption window title of the dialog (defaults to "")
+    @type str (optional)
+    @param directory working directory of the dialog (defaults to "")
+    @type str (optional)
+    @param filterStr filter string for the dialog (defaults to "")
+    @type str (optional)
+    @param initialFilter initial filter for the dialog (defaults to "")
+    @type str (optional)
+    @param withRemote flag indicating to create the file names with the remote
+        indicator (defaults to True)
+    @type bool (optional)
+    @return name of file to be opened
+    @rtype str
+    """
+    return getOpenFileNameAndFilter(
+        parent, caption, directory, filterStr, initialFilter, withRemote
+    )[0]
+
+
+def getOpenFileNameAndFilter(
+    parent=None,
+    caption="",
+    directory="",
+    filterStr="",
+    initialFilter="",
+    withRemote=True,
+):
+    """
+    Module function to get the name of a file for opening it and the selected
+    file name filter.
+
+    @param parent parent widget of the dialog (defaults to None)
+    @type QWidget (optional)
+    @param caption window title of the dialog (defaults to "")
+    @type str (optional)
+    @param directory working directory of the dialog (defaults to "")
+    @type str (optional)
+    @param filterStr filter string for the dialog (defaults to "")
+    @type str (optional)
+    @param initialFilter initial filter for the dialog (defaults to "")
+    @type str (optional)
+    @param withRemote flag indicating to create the file names with the remote
+        indicator (defaults to True)
+    @type bool (optional)
+    @return tuple containing the list of file names to be opened and the
+        selected file name filter
+    @rtype tuple of (list of str, str)
+    """
+    dlg = EricServerFileDialog(
+        parent=parent, caption=caption, directory=directory, filter=filterStr
+    )
+    dlg.setFileMode(FileMode.ExistingFile)
+    dlg.setNameFilter(initialFilter)
+    if dlg.exec() == QDialog.DialogCode.Accepted:
+        if withRemote:
+            fileName = FileSystemUtilities.remoteFileName(dlg.selectedFiles()[0])
+        else:
+            fileName = dlg.selectedFiles()[0]
+        selectedFilter = dlg.selectedNameFilter()
+    else:
+        fileName = ""
+        selectedFilter = ""
+
+    return fileName, selectedFilter
+
+
+def getOpenFileNames(
+    parent=None,
+    caption="",
+    directory="",
+    filterStr="",
+    initialFilter="",
+    withRemote=True,
+):
+    """
+    Module function to get a list of names of files for opening.
+
+    @param parent parent widget of the dialog (defaults to None)
+    @type QWidget (optional)
+    @param caption window title of the dialog (defaults to "")
+    @type str (optional)
+    @param directory working directory of the dialog (defaults to "")
+    @type str (optional)
+    @param filterStr filter string for the dialog (defaults to "")
+    @type str (optional)
+    @param initialFilter initial filter for the dialog (defaults to "")
+    @type str (optional)
+    @param withRemote flag indicating to create the file names with the remote
+        indicator (defaults to True)
+    @type bool (optional)
+    @return list of file names to be opened
+    @rtype list of str
+    """
+    return getOpenFileNamesAndFilter(
+        parent, caption, directory, filterStr, initialFilter, withRemote
+    )[0]
+
+
+def getOpenFileNamesAndFilter(
+    parent=None,
+    caption="",
+    directory="",
+    filterStr="",
+    initialFilter="",
+    withRemote=True,
+):
+    """
+    Module function to get a list of names of files for opening and the
+    selected file name filter.
+
+    @param parent parent widget of the dialog (defaults to None)
+    @type QWidget (optional)
+    @param caption window title of the dialog (defaults to "")
+    @type str (optional)
+    @param directory working directory of the dialog (defaults to "")
+    @type str (optional)
+    @param filterStr filter string for the dialog (defaults to "")
+    @type str (optional)
+    @param initialFilter initial filter for the dialog (defaults to "")
+    @type str (optional)
+    @param withRemote flag indicating to create the file names with the remote
+        indicator (defaults to True)
+    @type bool (optional)
+    @return tuple containing the list of file names to be opened and the
+        selected file name filter
+    @rtype tuple of (list of str, str)
+    """
+    dlg = EricServerFileDialog(
+        parent=parent, caption=caption, directory=directory, filter=filterStr
+    )
+    dlg.setFileMode(FileMode.ExistingFiles)
+    dlg.setNameFilter(initialFilter)
+    if dlg.exec() == QDialog.DialogCode.Accepted:
+        if withRemote:
+            filesList = [
+                FileSystemUtilities.remoteFileName(f) for f in dlg.selectedFiles()
+            ]
+        else:
+            filesList = dlg.selectedFiles()
+        selectedFilter = dlg.selectedNameFilter()
+    else:
+        filesList = []
+        selectedFilter = ""
+
+    return filesList, selectedFilter
+
+
+def getSaveFileName(
+    parent=None,
+    caption="",
+    directory="",
+    filterStr="",
+    initialFilter="",
+    withRemote=True,
+):
+    """
+    Module function to get the name of a file for saving.
+
+    @param parent parent widget of the dialog (defaults to None)
+    @type QWidget (optional)
+    @param caption window title of the dialog (defaults to "")
+    @type str (optional)
+    @param directory working directory of the dialog (defaults to "")
+    @type str (optional)
+    @param filterStr filter string for the dialog (defaults to "")
+    @type str (optional)
+    @param initialFilter initial filter for the dialog (defaults to "")
+    @type str (optional)
+    @param withRemote flag indicating to create the file names with the remote
+        indicator (defaults to True)
+    @type bool (optional)
+    @return name of file to be saved
+    @rtype str
+    """
+    return getSaveFileNameAndFilter(
+        parent, caption, directory, filterStr, initialFilter, withRemote
+    )[0]
+
+
+def getSaveFileNameAndFilter(
+    parent=None,
+    caption="",
+    directory="",
+    filterStr="",
+    initialFilter="",
+    withRemote=True,
+):
+    """
+    Module function to get the name of a file for saving and the selected file name
+    filter.
+
+    @param parent parent widget of the dialog (defaults to None)
+    @type QWidget (optional)
+    @param caption window title of the dialog (defaults to "")
+    @type str (optional)
+    @param directory working directory of the dialog (defaults to "")
+    @type str (optional)
+    @param filterStr filter string for the dialog (defaults to "")
+    @type str (optional)
+    @param initialFilter initial filter for the dialog (defaults to "")
+    @type str (optional)
+    @param withRemote flag indicating to create the file names with the remote
+        indicator (defaults to True)
+    @type bool (optional)
+    @return name of file to be saved and selected filte
+    @rtype tuple of (str, str)
+    """
+    dlg = EricServerFileDialog(
+        parent=parent, caption=caption, directory=directory, filter=filterStr
+    )
+    dlg.setFileMode(FileMode.AnyFile)
+    dlg.setNameFilter(initialFilter)
+    if dlg.exec() == QDialog.DialogCode.Accepted:
+        if withRemote:
+            fileName = FileSystemUtilities.remoteFileName(dlg.selectedFiles()[0])
+        else:
+            fileName = dlg.selectedFiles()[0]
+        selectedFilter = dlg.selectedNameFilter()
+    else:
+        fileName = ""
+        selectedFilter = ""
+
+    return fileName, selectedFilter
+
+
+def getExistingDirectory(
+    parent=None, caption="", directory="", dirsOnly=True, withRemote=True
+):
+    """
+    Module function to get the name of a directory.
+
+    @param parent parent widget of the dialog (defaults to None)
+    @type QWidget (optional)
+    @param caption window title of the dialog (defaults to "")
+    @type str (optional)
+    @param directory working directory of the dialog (defaults to "")
+    @type str (optional)
+    @param dirsOnly flag indicating to just show directories (defaults to True)
+    @type bool (optional)
+    @param withRemote flag indicating to create the file names with the remote
+        indicator (defaults to True)
+    @type bool (optional)
+    @return name of selected directory
+    @rtype str
+    """
+    dlg = EricServerFileDialog(parent=parent, caption=caption, directory=directory)
+    dlg.setFileMode(FileMode.Directory)
+    dlg.setDirectoriesOnly(dirsOnly)
+    if dlg.exec() == QDialog.DialogCode.Accepted:
+        if withRemote:
+            dirName = FileSystemUtilities.remoteFileName(dlg.selectedFiles()[0])
+        else:
+            dirName = dlg.selectedFiles()[0]
+    else:
+        dirName = ""
+
+    return dirName
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerFileDialog.ui	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EricServerFileDialog</class>
+ <widget class="QDialog" name="EricServerFileDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>450</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Open Files</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Look in:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="treeCombo">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="backButton">
+       <property name="toolTip">
+        <string>Press to move back in history.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="forwardButton">
+       <property name="toolTip">
+        <string>Press to move forward in history.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="upButton">
+       <property name="toolTip">
+        <string>Press to move up one level.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="newDirButton">
+       <property name="toolTip">
+        <string>Press to create a new directory.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="reloadButton">
+       <property name="toolTip">
+        <string>Press to reload the directory listing.</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QTreeWidget" name="listing">
+     <property name="contextMenuPolicy">
+      <enum>Qt::CustomContextMenu</enum>
+     </property>
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="rootIsDecorated">
+      <bool>false</bool>
+     </property>
+     <property name="itemsExpandable">
+      <bool>false</bool>
+     </property>
+     <property name="sortingEnabled">
+      <bool>false</bool>
+     </property>
+     <column>
+      <property name="text">
+       <string>Name</string>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>Size</string>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>Type</string>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>Date Modified</string>
+      </property>
+     </column>
+    </widget>
+   </item>
+   <item>
+    <layout class="QGridLayout" name="gridLayout">
+     <item row="0" column="0">
+      <widget class="QLabel" name="nameLabel">
+       <property name="text">
+        <string>File Name:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="QLineEdit" name="nameEdit">
+       <property name="clearButtonEnabled">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="2">
+      <widget class="QPushButton" name="okButton">
+       <property name="text">
+        <string>Open</string>
+       </property>
+       <property name="default">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="0">
+      <widget class="QLabel" name="filterLabel">
+       <property name="text">
+        <string>Files of Type:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1">
+      <widget class="QComboBox" name="filterCombo">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="2">
+      <widget class="QPushButton" name="cancelButton">
+       <property name="text">
+        <string>Cancel</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>nameEdit</tabstop>
+  <tabstop>filterCombo</tabstop>
+  <tabstop>okButton</tabstop>
+  <tabstop>cancelButton</tabstop>
+  <tabstop>treeCombo</tabstop>
+  <tabstop>backButton</tabstop>
+  <tabstop>forwardButton</tabstop>
+  <tabstop>upButton</tabstop>
+  <tabstop>newDirButton</tabstop>
+  <tabstop>reloadButton</tabstop>
+  <tabstop>listing</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,1102 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the file system interface to the eric-ide server.
+"""
+
+import base64
+import contextlib
+import os
+import re
+import stat
+
+from PyQt6.QtCore import QEventLoop, QObject, pyqtSlot
+
+from eric7 import Utilities
+from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
+from eric7.SystemUtilities import FileSystemUtilities
+
+
+class EricServerFileSystemInterface(QObject):
+    """
+    Class implementing the file system interface to the eric-ide server.
+    """
+
+    _MagicCheck = re.compile("([*?[])")
+
+    def __init__(self, serverInterface):
+        """
+        Constructor
+
+        @param serverInterface reference to the eric-ide server interface
+        @type EricServerInterface
+        """
+        super().__init__(parent=serverInterface)
+
+        self.__serverInterface = serverInterface
+        self.__serverInterface.connectionStateChanged.connect(
+            self.__connectionStateChanged
+        )
+
+        self.__serverPathSep = self.__getPathSep()
+
+    def __hasMagic(self, pathname):
+        """
+        Private method to check, if a given path contains glob style magic characters.
+
+        Note: This was taken from 'glob.glob'.
+
+        @param pathname path name to be checked
+        @type str
+        @return flag indicating the presence of magic characters
+        @rtype bool
+        """
+        match = self._MagicCheck.search(pathname)
+        return match is not None
+
+    @pyqtSlot(bool)
+    def __connectionStateChanged(self, connected):
+        """
+        Private slot handling a change of the server connection state.
+
+        @param connected flag indicating a connected state
+        @type bool
+        """
+        if connected:
+            if not bool(self.__serverPathSep):
+                self.__serverPathSep = self.__getPathSep()
+        else:
+            self.__serverPathSep = ""
+
+    def __getPathSep(self):
+        """
+        Private method to get the path separator of the connected server.
+
+        @return path separator character of the server
+        @rtype str
+        """
+        loop = QEventLoop()
+        sep = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal sep
+
+            if reply == "GetPathSep":
+                sep = params["separator"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="GetPathSep",
+                params={},
+                callback=callback,
+            )
+
+            loop.exec()
+
+        return sep
+
+    def getcwd(self):
+        """
+        Public method to get the current working directory of the eric-ide server.
+
+        @return current working directory of the eric-ide server
+        @rtype str
+        """
+        loop = QEventLoop()
+        cwd = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal cwd
+
+            if reply == "Getcwd":
+                cwd = params["directory"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="Getcwd",
+                params={},
+                callback=callback,
+            )
+
+            loop.exec()
+
+        return cwd
+
+    def chdir(self, directory):
+        """
+        Public method to change the current working directory of the eric-ide server.
+
+        @param directory absolute path of the working directory to change to
+        @type str
+        @return tuple containing an OK flag and an error string in case of an issue
+        @rtype tuple of (bool, str)
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error
+
+            if reply == "Chdir":
+                ok = params["ok"]
+                with contextlib.suppress(KeyError):
+                    error = params["error"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="Chdir",
+                params={"directory": FileSystemUtilities.plainFileName(directory)},
+                callback=callback,
+            )
+
+            loop.exec()
+            return ok, error
+
+        else:
+            return False, "Not connected to an 'eric-ide' server."
+
+    def listdir(self, directory=""):
+        """
+        Public method to get a directory listing.
+
+        @param directory directory to be listed. An empty directory means to list
+            the eric-ide server current directory. (defaults to "")
+        @type str (optional)
+        @return tuple containing the listed directory, the path separartor and the
+            directory listing. Each directory listing entry contains a dictionary
+            with the relevant data.
+        @rtype tuple of (str, str, dict)
+        @exception OSError raised in case the server reported an issue
+        """
+        if directory is None:
+            # sanitize the directory in case it is None
+            directory = ""
+
+        loop = QEventLoop()
+        ok = False
+        error = ""
+        listedDirectory = ""
+        separator = ""
+        listing = []
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal listedDirectory, listing, separator, ok, error
+
+            if reply == "Listdir":
+                ok = params["ok"]
+                if ok:
+                    listedDirectory = params["directory"]
+                    listing = params["listing"]
+                    separator = params["separator"]
+                else:
+                    error = params["error"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="Listdir",
+                params={"directory": FileSystemUtilities.plainFileName(directory)},
+                callback=callback,
+            )
+
+            loop.exec()
+            if not ok:
+                raise OSError(error)
+
+        return listedDirectory, separator, listing
+
+    def direntries(
+        self,
+        directory,
+        filesonly=False,
+        pattern=None,
+        followsymlinks=True,
+        ignore=None,
+        recursive=True,
+        dirsonly=False,
+    ):
+        """
+        Public method to get a list of all files and directories of a given directory.
+
+        @param directory root of the tree to check
+        @type str
+        @param filesonly flag indicating that only files are wanted (defaults to False)
+        @type bool (optional)
+        @param pattern a filename pattern or list of filename patterns to check
+            against (defaults to None)
+        @type str or list of str (optional)
+        @param followsymlinks flag indicating whether symbolic links should be
+            followed (defaults to True)
+        @type bool (optional)
+        @param ignore list of entries to be ignored (defaults to None)
+        @type list of str (optional)
+        @param recursive flag indicating a recursive search (defaults to True)
+        @type bool (optional)
+        @param dirsonly flag indicating to return only directories. When True it has
+            precedence over the 'filesonly' parameter ((defaults to False)
+        @type bool
+        @return list of all files and directories in the tree rooted at path.
+            The names are expanded to start with the given directory name.
+        @rtype list of str
+        @exception OSError raised in case the server reported an issue
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+        result = []
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal result, ok, error
+
+            if reply == "DirEntries":
+                ok = params["ok"]
+                if ok:
+                    result = params["result"]
+                else:
+                    error = params["error"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="DirEntries",
+                params={
+                    "directory": FileSystemUtilities.plainFileName(directory),
+                    "files_only": filesonly,
+                    "pattern": [] if pattern is None else pattern,
+                    "follow_symlinks": followsymlinks,
+                    "ignore": [] if ignore is None else ignore,
+                    "recursive": recursive,
+                    "dirs_only": dirsonly,
+                },
+                callback=callback,
+            )
+
+            loop.exec()
+            if not ok:
+                raise OSError(error)
+
+        return result
+
+    def glob(self, pathname, recursive=False, includeHidden=False):
+        """
+        Public method to get a list of of all files matching a given pattern
+        like 'glob.glob()'.
+
+        @param pathname path name pattern with simple shell-style wildcards
+        @type str
+        @param recursive flag indicating a recursive list (defaults to False)
+        @type bool (optional)
+        @param includeHidden flag indicating to include hidden files (defaults to False)
+        @type bool (optional)
+        @return list of all files matching the pattern
+        @rtype list of str
+        """
+        result = []
+
+        pathname = FileSystemUtilities.plainFileName(pathname)
+        dirname, basename = os.path.split(pathname)
+        if dirname and not self.__hasMagic(dirname):
+            with contextlib.suppress(OSError):
+                entries = self.direntries(
+                    dirname, pattern=basename, recursive=recursive, filesonly=True
+                )
+                result = (
+                    entries
+                    if includeHidden
+                    else [e for e in entries if not e.startswith(".")]
+                )
+
+        return result
+
+    def stat(self, filename, stNames):
+        """
+        Public method to get the status of a file.
+
+        @param filename name of the file
+        @type str
+        @param stNames list of 'stat_result' members to retrieve
+        @type list of str
+        @return dictionary containing the requested status data
+        @rtype dict
+        @exception OSError raised in case the server reported an issue
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+        stResult = {}
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error, stResult
+
+            if reply == "Stat":
+                ok = params["ok"]
+                if ok:
+                    stResult = params["result"]
+                else:
+                    error = params["error"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="Stat",
+                params={
+                    "filename": FileSystemUtilities.plainFileName(filename),
+                    "st_names": stNames,
+                },
+                callback=callback,
+            )
+
+            loop.exec()
+            if not ok:
+                raise OSError(error)
+
+        return stResult
+
+    def isdir(self, name):
+        """
+        Public method to check, if the given name is a directory.
+
+        @param name name to be checked
+        @type str
+        @return flag indicating a directory
+        @rtype bool
+        """
+        with contextlib.suppress(KeyError, OSError):
+            result = self.stat(name, ["st_mode"])
+            return stat.S_ISDIR(result["st_mode"])
+
+        return False
+
+    def isfile(self, name):
+        """
+        Public method to check, if the given name is a regular file.
+
+        @param name name to be checked
+        @type str
+        @return flag indicating a regular file
+        @rtype bool
+        """
+        with contextlib.suppress(KeyError, OSError):
+            result = self.stat(name, ["st_mode"])
+            return stat.S_ISREG(result["st_mode"])
+
+        return False
+
+    def exists(self, name):
+        """
+        Public method the existence of a file or directory.
+
+        @param name name of the file or directory
+        @type str
+        @return flag indicating the file existence
+        @rtype bool
+        """
+        loop = QEventLoop()
+        nameExists = False
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal nameExists
+
+            if reply == "Exists":
+                nameExists = params["exists"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="Exists",
+                params={"name": FileSystemUtilities.plainFileName(name)},
+                callback=callback,
+            )
+
+            loop.exec()
+
+        return nameExists
+
+    def access(self, name, modes):
+        """
+        Public method to test the given access rights to a file or directory.
+
+        The modes to check for are 'read', 'write' or 'execute' or any combination.
+
+        @param name name of the file or directory
+        @type str
+        @param modes list of modes to check for
+        @type str or list of str
+        @return flag indicating the user has the asked for permissions
+        @rtype bool
+        @exception ValueError raised for an illegal modes list
+        """
+        if not modes:
+            raise ValueError(
+                "At least one of 'read', 'write' or 'execute' must be specified."
+            )
+
+        if isinstance(modes, str):
+            # convert to a list with one element
+            modes = [modes]
+
+        loop = QEventLoop()
+        accessOK = False
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal accessOK
+
+            if reply == "Access":
+                accessOK = params["ok"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="Access",
+                params={
+                    "name": FileSystemUtilities.plainFileName(name),
+                    "modes": modes,
+                },
+                callback=callback,
+            )
+
+            loop.exec()
+
+        return accessOK
+
+    def mkdir(self, directory):
+        """
+        Public method to create a new directory on the eric-ide server.
+
+        @param directory absolute path of the new directory
+        @type str
+        @return tuple containing an OK flag and an error string in case of an issue
+        @rtype tuple of (bool, str)
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error
+
+            if reply == "Mkdir":
+                ok = params["ok"]
+                with contextlib.suppress(KeyError):
+                    error = params["error"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="Mkdir",
+                params={"directory": FileSystemUtilities.plainFileName(directory)},
+                callback=callback,
+            )
+
+            loop.exec()
+            return ok, error
+
+        else:
+            return False, "Not connected to an 'eric-ide' server."
+
+    def makedirs(self, directory, exist_ok=False):
+        """
+        Public method to create a new directory on the eric-ide serverincluding all
+        intermediate-level directories.
+
+        @param directory absolute path of the new directory
+        @type str
+        @param exist_ok flag indicating that the existence of the directory is
+            acceptable (defaults to False)
+        @type bool (optional)
+        @return tuple containing an OK flag and an error string in case of an issue
+        @rtype tuple of (bool, str)
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error
+
+            if reply == "MakeDirs":
+                ok = params["ok"]
+                with contextlib.suppress(KeyError):
+                    error = params["error"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="MakeDirs",
+                params={
+                    "directory": FileSystemUtilities.plainFileName(directory),
+                    "exist_ok": exist_ok,
+                },
+                callback=callback,
+            )
+
+            loop.exec()
+            return ok, error
+
+        else:
+            return False, "Not connected to an 'eric-ide' server."
+
+    def rmdir(self, directory):
+        """
+        Public method to delete a directory on the eric-ide server.
+
+        @param directory absolute path of the directory
+        @type str
+        @return tuple containing an OK flag and an error string in case of an issue
+        @rtype tuple of (bool, str)
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error
+
+            if reply == "Rmdir":
+                ok = params["ok"]
+                with contextlib.suppress(KeyError):
+                    error = params["error"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="Rmdir",
+                params={"directory": FileSystemUtilities.plainFileName(directory)},
+                callback=callback,
+            )
+
+            loop.exec()
+            return ok, error
+
+        else:
+            return False, "Not connected to an 'eric-ide' server."
+
+    def replace(self, oldName, newName):
+        """
+        Public method to rename a file or directory.
+
+        @param oldName current name of the file or directory
+        @type str
+        @param newName new name for the file or directory
+        @type str
+        @return tuple containing an OK flag and an error string in case of an issue
+        @rtype tuple of (bool, str)
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error
+
+            if reply == "Replace":
+                ok = params["ok"]
+                with contextlib.suppress(KeyError):
+                    error = params["error"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="Replace",
+                params={
+                    "old_name": FileSystemUtilities.plainFileName(oldName),
+                    "new_name": FileSystemUtilities.plainFileName(newName),
+                },
+                callback=callback,
+            )
+
+            loop.exec()
+            return ok, error
+
+        else:
+            return False, "Not connected to an 'eric-ide' server."
+
+    def remove(self, filename):
+        """
+        Public method to delete a file on the eric-ide server.
+
+        @param filename absolute path of the file
+        @type str
+        @return tuple containing an OK flag and an error string in case of an issue
+        @rtype tuple of (bool, str)
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error
+
+            if reply == "Remove":
+                ok = params["ok"]
+                with contextlib.suppress(KeyError):
+                    error = params["error"]
+                loop.quit()
+
+        if self.__serverInterface.isServerConnected():
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="Remove",
+                params={"filename": FileSystemUtilities.plainFileName(filename)},
+                callback=callback,
+            )
+
+            loop.exec()
+            return ok, error
+
+        else:
+            return False, "Not connected to an 'eric-ide' server."
+
+    #######################################################################
+    ## Methods for splitting or joining remote path names.
+    ##
+    ## These are simplified variants of the os.path functions. If the
+    ## 'eric-ide' server is not connected, the os.path functions are used.
+    #######################################################################
+
+    def separator(self):
+        """
+        Public method to return the server side path separator string.
+
+        @return path separator
+        @rtype str
+        """
+        return self.__serverPathSep
+
+    def join(self, a, *p):
+        """
+        Public method to join two or more path name components using the path separator
+        of the server side.
+
+        @param a first path component
+        @type str
+        @param *p list of additional path components
+        @type list of str
+        @return joined path name
+        @rtype str
+        """
+        if self.__serverInterface.isServerConnected():
+            path = a
+            for b in p:
+                if b.startswith(self.__serverPathSep):
+                    path = b
+                elif not path or path.endswith(self.__serverPathSep):
+                    path += b
+                else:
+                    path += self.__serverPathSep + b
+            return path
+
+        else:
+            return os.path.join(a, *p)
+
+    def split(self, p):
+        """
+        Public method to split a path name.
+
+        @param p path name to be split
+        @type str
+        @return tuple containing head and tail, where tail is everything after the last
+            path separator.
+        @rtype tuple of (str, str)
+        """
+        if self.__serverInterface.isServerConnected():
+            if self.__serverPathSep == "\\":
+                # remote is a Windows system
+                normp = p.replace("/", "\\")
+            else:
+                # remote is a Posix system
+                normp = p.replace("\\", "/")
+
+            i = normp.rfind(self.__serverPathSep) + 1
+            head, tail = normp[:i], normp[i:]
+            if head and head != self.__serverPathSep * len(head):
+                head = head.rstrip(self.__serverPathSep)
+            return head, tail
+
+        else:
+            return os.path.split(p)
+
+    def splitext(self, p):
+        """
+        Public method to split a path name into a root part and an extension.
+
+        @param p path name to be split
+        @type str
+        @return tuple containing the root part and the extension
+        @rtype tuple of (str, str)
+        """
+        return os.path.splitext(p)
+
+    def splitdrive(self, p):
+        """
+        Public method to split a path into drive and path.
+
+        @param p path name to be split
+        @type str
+        @return tuple containing the drive letter (incl. colon) and the path
+        @rtype tuple of (str, str)
+        """
+        if self.__serverInterface.isServerConnected():
+            plainp = FileSystemUtilities.plainFileName(p)
+
+            if self.__serverPathSep == "\\":
+                # remote is a Windows system
+                normp = plainp.replace("/", "\\")
+                if normp[1:2] == ":":
+                    return normp[:2], normp[2:]
+                else:
+                    return "", normp
+            else:
+                # remote is a Posix system
+                normp = plainp.replace("\\", "/")
+                return "", normp
+
+        else:
+            return os.path.splitdrive(p)
+
+    def dirname(self, p):
+        """
+        Public method to extract the directory component of a path name.
+
+        @param p path name
+        @type str
+        @return directory component
+        @rtype str
+        """
+        return self.split(p)[0]
+
+    def basename(self, p):
+        """
+        Public method to extract the final component of a path name.
+
+        @param p path name
+        @type str
+        @return final component
+        @rtype str
+        """
+        return self.split(p)[1]
+
+    def toNativeSeparators(self, p):
+        """
+        Public method to convert a path to use server native separator characters.
+
+        @param p path name to be converted
+        @type str
+        @return path name with converted separator characters
+        @rtype str
+        """
+        if self.__serverPathSep == "/":
+            return p.replace("\\", "/")
+        else:
+            return p.replace("/", "\\")
+
+    def fromNativeSeparators(self, p):
+        """
+        Public method to convert a path using server native separator characters to
+        use "/" separator characters.
+
+        @param p path name to be converted
+        @type str
+        @return path name with converted separator characters
+        @rtype str
+        """
+        return p.replace(self.__serverPathSep, "/")
+
+    #######################################################################
+    ## Methods for reading and writing files
+    #######################################################################
+
+    def readFile(self, filename, create=False):
+        """
+        Public method to read a file from the eric-ide server.
+
+        @param filename name of the file to read
+        @type str
+        @param create flag indicating to create an empty file, if it does not exist
+            (defaults to False)
+        @type bool (optional)
+        @return bytes data read from the eric-ide server
+        @rtype bytes
+        @exception OSError raised in case the server reported an issue
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+        bText = b""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error, bText
+
+            if reply == "ReadFile":
+                ok = params["ok"]
+                if ok:
+                    bText = base64.b85decode(
+                        bytes(params["filedata"], encoding="ascii")
+                    )
+                else:
+                    error = params["error"]
+                loop.quit()
+
+        if not self.__serverInterface.isServerConnected():
+            raise OSError("Not connected to an 'eric-ide' server.")
+
+        else:
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="ReadFile",
+                params={
+                    "filename": FileSystemUtilities.plainFileName(filename),
+                    "create": create,
+                },
+                callback=callback,
+            )
+
+            loop.exec()
+            if not ok:
+                raise OSError(error)
+
+            return bText
+
+    def writeFile(self, filename, data, withBackup=False):
+        """
+        Public method to write the data to a file on the eric-ide server.
+
+        @param filename name of the file to write
+        @type str
+        @param data data to be written
+        @type bytes
+        @param withBackup flag indicating to create a backup file first
+            (defaults to False)
+        @type bool (optional)
+        @exception OSError raised in case the server reported an issue
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error
+
+            if reply == "WriteFile":
+                ok = params["ok"]
+                with contextlib.suppress(KeyError):
+                    error = params["error"]
+                loop.quit()
+
+        if not self.__serverInterface.isServerConnected():
+            raise OSError("Not connected to an 'eric-ide' server.")
+
+        else:
+            self.__serverInterface.sendJson(
+                category=EricRequestCategory.FileSystem,
+                request="WriteFile",
+                params={
+                    "filename": FileSystemUtilities.plainFileName(filename),
+                    "filedata": str(base64.b85encode(data), encoding="ascii"),
+                    "with_backup": withBackup,
+                },
+                callback=callback,
+            )
+
+            loop.exec()
+            if not ok:
+                raise OSError(error)
+
+    def readEncodedFile(self, filename, create=False):
+        """
+        Public method to read a file and decode its contents into proper text.
+
+        @param filename name of the file to read
+        @type str
+        @param create flag indicating to create an empty file, if it does not exist
+            (defaults to False)
+        @type bool (optional)
+        @return tuple of decoded text and encoding
+        @rtype tuple of (str, str)
+        """
+        data = self.readFile(filename, create=create)
+        return Utilities.decode(data)
+
+    def readEncodedFileWithEncoding(self, filename, encoding, create=False):
+        """
+        Public method to read a file and decode its contents into proper text.
+
+        @param filename name of the file to read
+        @type str
+        @param encoding encoding to be used to read the file
+        @type str
+        @param create flag indicating to create an empty file, if it does not exist
+            (defaults to False)
+        @type bool (optional)
+        @return tuple of decoded text and encoding
+        @rtype tuple of (str, str)
+        """
+        data = self.readFile(filename, create=create)
+        return Utilities.decodeWithEncoding(data, encoding)
+
+    def writeEncodedFile(
+        self, filename, text, origEncoding, forcedEncoding="", withBackup=False
+    ):
+        """
+        Public method to write a file with properly encoded text.
+
+        @param filename name of the file to read
+        @type str
+        @param text text to be written
+        @type str
+        @param origEncoding type of the original encoding
+        @type str
+        @param forcedEncoding encoding to be used for writing, if no coding
+            line is present (defaults to "")
+        @type str (optional)
+        @param withBackup flag indicating to create a backup file first
+            (defaults to False)
+        @type bool (optional)
+        @return encoding used for writing the file
+        @rtype str
+        """
+        data, encoding = Utilities.encode(
+            text, origEncoding, forcedEncoding=forcedEncoding
+        )
+        self.writeFile(filename, data, withBackup=withBackup)
+
+        return encoding
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerInterface.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,763 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the interface to the eric remote server.
+"""
+
+import collections
+import json
+import struct
+import uuid
+import zlib
+
+from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
+from PyQt6.QtGui import QAction, QKeySequence
+from PyQt6.QtNetwork import QAbstractSocket, QTcpSocket
+from PyQt6.QtWidgets import QDialog, QMenu, QToolBar, QToolButton
+
+from eric7 import Preferences, Utilities
+from eric7.EricGui import EricPixmapCache
+from eric7.EricGui.EricAction import EricAction
+from eric7.EricWidgets import EricMessageBox
+from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
+
+
+class EricServerInterface(QObject):
+    """
+    Class implementing the interface to the eric remote server.
+
+    @signal showMenu(name:str, menu:QMenu) emitted when a menu is about to be shown.
+        The name of the menu and a reference to the menu are given.
+
+    @signal connectionStateChanged(state:bool) emitted to indicate a change of the
+        connection state
+    @signal aboutToDisconnect() emitted just befor the remote server is disconnected
+
+    @signal remoteReply(category:int, request:str, params:dict) emitted to deliver the
+        reply of an unknown category
+    @signal remoteCoverageReply(request:str, params:dict) emitted to deliver the reply
+        of a remote server code coverage request
+    @signal remoteDebuggerReply(request:str, params:dict) emitted to deliver the reply
+        of a remote server debugger request
+    @signal remoteEchoReply(request:str, params:dict) emitted to deliver the reply of
+        a remote server echo request
+    @signal remoteFileSystemReply(request:str, params:dict) emitted to deliver the
+        reply of a remote server file system request
+    @signal remoteProjectReply(request:str, params:dict) emitted to deliver the reply
+        of a remote server project related request
+    @signal remoteServerReply(request:str, params:dict) emitted to deliver the reply
+        of a remote server control request
+    """
+
+    showMenu = pyqtSignal(str, QMenu)
+
+    aboutToDisconnect = pyqtSignal()
+    connectionStateChanged = pyqtSignal(bool)
+
+    remoteReply = pyqtSignal(int, str, dict)
+
+    remoteCoverageReply = pyqtSignal(str, dict)
+    remoteDebuggerReply = pyqtSignal(str, dict)
+    remoteEchoReply = pyqtSignal(str, dict)
+    remoteFileSystemReply = pyqtSignal(str, dict)
+    remoteProjectReply = pyqtSignal(str, dict)
+    remoteServerReply = pyqtSignal(str, dict)
+
+    def __init__(self, parent=None):
+        """
+        Constructor
+
+        @param parent reference to the parent object (defaults to None)
+        @type QObject (optional)
+        """
+        super().__init__(parent=parent)
+
+        self.__ui = parent
+
+        self.__categorySignalMapping = {
+            EricRequestCategory.Coverage: self.remoteCoverageReply,
+            EricRequestCategory.Debugger: self.remoteDebuggerReply,
+            EricRequestCategory.Echo: self.remoteEchoReply,
+            EricRequestCategory.FileSystem: self.remoteFileSystemReply,
+            EricRequestCategory.Project: self.remoteProjectReply,
+            EricRequestCategory.Server: self.remoteServerReply,
+        }
+        self.__serviceInterfaces = {}
+        # no specific service interfaces have been created yet
+
+        self.__connection = None
+        self.__callbacks = {}  # callback references indexed by UUID
+        self.__messageQueue = collections.deque()
+        self.__connected = False
+
+        self.connectionStateChanged.connect(self.__connectionStateChanged)
+
+    def getServiceInterface(self, name):
+        """
+        Public method to get a references to a specific service interface by
+        service name.
+
+        @param name service name
+        @type str
+        @return reference to the service interface
+        @rtype QObject
+        @exception ValueError raised to indicate an unsupported server interface
+            was requested
+        """
+        lname = name.lower()
+        try:
+            return self.__serviceInterfaces[lname]
+        except KeyError:
+            if lname not in ("coverage", "debugger", "filesystem", "project"):
+                raise ValueError(f"no such service supported ({name})")
+            else:
+                # instantiate the service interface
+                if lname == "filesystem":
+                    from .EricServerFileSystemInterface import (  # noqa: I101
+                        EricServerFileSystemInterface,
+                    )
+
+                    self.__serviceInterfaces[lname] = EricServerFileSystemInterface(
+                        self
+                    )
+                elif lname == "debugger":
+                    from .EricServerDebuggerInterface import (  # noqa: I101
+                        EricServerDebuggerInterface,
+                    )
+
+                    self.__serviceInterfaces[lname] = EricServerDebuggerInterface(self)
+                elif lname == "coverage":
+                    from .EricServerCoverageInterface import (  # noqa: I101
+                        EricServerCoverageInterface,
+                    )
+
+                    self.__serviceInterfaces[lname] = EricServerCoverageInterface(self)
+                elif lname == "project":
+                    # TODO: 'Project Interface' not implemented yet
+                    pass
+
+            return self.__serviceInterfaces[lname]
+
+    #######################################################################
+    ## Methods for handling the server connection.
+    #######################################################################
+
+    def connectToServer(self, host, port=None, timeout=None):
+        """
+        Public method to connect to the given host and port.
+
+        @param host host name or IP address of the eric remote server
+        @type str
+        @param port port number to connect to (defaults to None)
+        @type int (optional)
+        @param timeout timeout im seconds for the connection attempt
+            (defaults to None)
+        @type int (optional)
+        @return flag indicating success
+        @rtype bool
+        """
+        if not bool(port):  # None or 0
+            # use default port
+            port = 42024
+
+        if not bool(timeout):  # None or 0
+            # use configured default timeout
+            timeout = Preferences.getEricServer("ConnectionTimeout")
+        timeout *= 1000  # convert to milliseconds
+
+        if self.__connection is not None:
+            self.disconnectFromServer()
+
+        self.__connection = QTcpSocket(self)
+        self.__connection.connectToHost(host, port)
+        if not self.__connection.waitForConnected(timeout):
+            EricMessageBox.critical(
+                None,
+                self.tr("Connect to eric-ide Server"),
+                self.tr(
+                    "<p>The connection to the eric-ide server {0}:{1} could not be"
+                    " established.</p><p>Reason: {2}</p>"
+                ).format(
+                    host if ":" not in host else f"[{host}]",
+                    port,
+                    self.__connection.errorString(),
+                ),
+            )
+
+            self.__connection = None
+            return False
+
+        self.__connection.readyRead.connect(self.__receiveJson)
+        self.__connection.disconnected.connect(self.__handleDisconnect)
+
+        self.connectionStateChanged.emit(True)
+
+        return True
+
+    @pyqtSlot()
+    def disconnectFromServer(self):
+        """
+        Public method to disconnect from the eric remote server.
+        """
+        if self.__connection is not None and self.__connection.isValid():
+            # signal we are abouzt to disconnect
+            self.aboutToDisconnect.emit()
+
+            # disconnect from the eric-ide server
+            self.__connection.disconnectFromHost()
+            if self.__connection is not None:
+                # may have disconnected already
+                self.__connection.waitForDisconnected(
+                    Preferences.getEricServer("ConnectionTimeout") * 1000
+                )
+
+                self.connectionStateChanged.emit(False)
+                self.__connection = None
+                self.__callbacks.clear()
+
+    def isServerConnected(self):
+        """
+        Public method to check, if a connection to an eric-ide server has been
+        established.
+
+        @return flag indicating the interface connection state
+        @rtype bool
+        """
+        return (
+            self.__connection is not None
+            and self.__connection.state() == QAbstractSocket.SocketState.ConnectedState
+        )
+
+    @pyqtSlot()
+    def __handleDisconnect(self):
+        """
+        Private slot handling a disconnect of the client.
+        """
+        if self.__connection is not None:
+            self.__connection.close()
+
+        self.connectionStateChanged.emit(False)
+        self.__connection = None
+        self.__callbacks.clear()
+
+    #######################################################################
+    ## Methods for sending requests and receiving the replies.
+    #######################################################################
+
+    @pyqtSlot()
+    def __receiveJson(self):
+        """
+        Private slot handling received data from the eric remote server.
+        """
+        while self.__connection and self.__connection.bytesAvailable():
+            header = self.__connection.read(struct.calcsize(b"!II"))
+            length, datahash = struct.unpack(b"!II", header)
+
+            data = bytearray()
+            while len(data) < length:
+                maxSize = length - len(data)
+                if self.__connection.bytesAvailable() < maxSize:
+                    self.__connection.waitForReadyRead(50)
+                if not self.__connection:
+                    # connection to server is gone uncontrolled
+                    break
+                newData = self.__connection.read(maxSize)
+                if newData:
+                    data += newData
+
+            if zlib.adler32(data) & 0xFFFFFFFF != datahash:
+                # corrupted data -> discard and continue
+                continue
+
+            jsonString = data.decode("utf-8", "backslashreplace")
+
+            # - print("Remote Server Interface Receive: {0}".format(jsonString))
+            # - this is for debugging only
+
+            try:
+                serverDataDict = json.loads(jsonString.strip())
+            except (TypeError, ValueError) as err:
+                EricMessageBox.critical(
+                    None,
+                    self.tr("JSON Protocol Error"),
+                    self.tr(
+                        """<p>The response received from the remote server"""
+                        """ could not be decoded. Please report"""
+                        """ this issue with the received data to the"""
+                        """ eric bugs email address.</p>"""
+                        """<p>Error: {0}</p>"""
+                        """<p>Data:<br/>{1}</p>"""
+                    ).format(str(err), Utilities.html_encode(jsonString.strip())),
+                    EricMessageBox.Ok,
+                )
+                return
+
+            reqUuid = serverDataDict["uuid"]
+            if reqUuid:
+                # It is a response to a synchronous request -> handle the call back
+                # immediately.
+                self.__callbacks[reqUuid](
+                    serverDataDict["reply"], serverDataDict["params"]
+                )
+                del self.__callbacks[reqUuid]
+            else:
+                self.__messageQueue.append(serverDataDict)
+
+        while self.__messageQueue:
+            serverDataDict = self.__messageQueue.popleft()  # get the first message
+            try:
+                self.__categorySignalMapping[serverDataDict["category"]].emit(
+                    serverDataDict["reply"], serverDataDict["params"]
+                )
+            except KeyError:
+                if serverDataDict["category"] == EricRequestCategory.Error:
+                    # handle server errors in here
+                    self.__handleServerError(
+                        serverDataDict["reply"], serverDataDict["params"]
+                    )
+                else:
+                    self.remoteReply.emit(
+                        serverDataDict["category"],
+                        serverDataDict["reply"],
+                        serverDataDict["params"],
+                    )
+
+    def sendJson(self, category, request, params, callback=None, flush=False):
+        """
+        Public method to send a single command to a client.
+
+        @param category service category
+        @type EricRequestCategory
+        @param request request name to be sent
+        @type str
+        @param params dictionary of named parameters for the request
+        @type dict
+        @param callback callback function for the reply from the eric remote server
+            (defaults to None)
+        @type function (optional)
+        @param flush flag indicating to flush the data to the socket
+            (defaults to False)
+        @type bool (optional)
+        """
+        if callback:
+            reqUuid = str(uuid.uuid4())
+            self.__callbacks[reqUuid] = callback
+        else:
+            reqUuid = ""
+
+        serviceDict = {
+            "jsonrpc": "2.0",
+            "category": category,
+            "request": request,
+            "params": params,
+            "uuid": reqUuid,
+        }
+        jsonString = json.dumps(serviceDict) + "\n"
+
+        # - print("Remote Server Interface Send: {0}".format(jsonString))
+        # - this is for debugging only
+
+        if self.__connection is not None:
+            data = jsonString.encode("utf8", "backslashreplace")
+            header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF)
+            self.__connection.write(header)
+            self.__connection.write(data)
+            if flush:
+                self.__connection.flush()
+
+    def shutdownServer(self):
+        """
+        Public method shutdown the currebtly connected eric-ide remote server.
+        """
+        if self.__connection:
+            self.sendJson(
+                category=EricRequestCategory.Server,
+                request="Shutdown",
+                params={},
+            )
+
+    @pyqtSlot()
+    def serverVersions(self):
+        """
+        Public slot to request the eric-ide version of the server.
+        """
+        if self.__connection:
+            self.sendJson(
+                category=EricRequestCategory.Server,
+                request="Versions",
+                params={},
+                callback=self.__handleServerVersionReply,
+            )
+
+    #######################################################################
+    ## Callback methods
+    #######################################################################
+
+    def __handleServerVersionReply(self, reply, params):
+        """
+        Private method to handle the reply of a 'Version' request.
+
+        @param reply name of the eric-ide server reply
+        @type str
+        @param params dictionary containing the reply data
+        @type dict
+        @exception ValueError raised in case of an unsupported reply
+        """
+        if reply != "Versions":
+            raise ValueError(f"unsupported reply received ({reply})")
+
+        else:
+            hostname = params["hostname"]
+            versionText = self.tr("<h2>{0}Version Numbers</h2><table>").format(
+                self.tr("{0} - ").format(hostname) if hostname else ""
+            )
+
+            # Python version
+            versionText += (
+                """<tr><td><b>Python</b></td><td>{0}, {1}</td></tr>"""
+            ).format(params["python"], params["py_bitsize"])
+
+            # eric7 version
+            versionText += (
+                """<tr><td><b>eric7_server</b></td><td>{0}</td></tr>"""
+            ).format(params["version"])
+
+            versionText += self.tr("""</table>""")
+
+            EricMessageBox.about(
+                None,
+                self.tr("eric-ide Server Versions"),
+                versionText,
+            )
+
+    #######################################################################
+    ## Reply handler methods
+    #######################################################################
+
+    def __handleServerError(self, reply, params):
+        """
+        Private method handling server error replies.
+
+        @param reply name of the error reply
+        @type str
+        @param params dictionary containing the specific reply data
+        @type dict
+        """
+        if reply == "ClientChecksumException":
+            self.__ui.appendToStderr(
+                self.tr(
+                    "eric-ide Server Checksum Error\nError: {0}\nData:\n{1}\n"
+                ).format(params["ExceptionValue"], params["ProtocolData"])
+            )
+
+        elif reply == "ClientException":
+            self.__ui.appendToStderr(
+                self.tr("eric-ide Server Data Error\nError: {0}\nData:\n{1}\n").format(
+                    params["ExceptionValue"], params["ProtocolData"]
+                )
+            )
+
+        elif reply == "UnsupportedServiceCategory":
+            self.__ui.appendToStderr(
+                self.tr(
+                    "eric-ide Server Unsupported Category\n"
+                    "Error: The server received the unsupported request category '{0}'."
+                ).format(params["Category"])
+            )
+
+    #######################################################################
+    ## User interface related methods
+    #######################################################################
+
+    def initActions(self):
+        """
+        Public slot to initialize the eric-ide server actions.
+        """
+        self.actions = []
+
+        self.connectServerAct = EricAction(
+            self.tr("Connect"),
+            EricPixmapCache.getIcon("linkConnect"),
+            self.tr("Connect..."),
+            QKeySequence(self.tr("Meta+Shift+C")),
+            0,
+            self,
+            "remote_server_connect",
+        )
+        self.connectServerAct.setStatusTip(
+            self.tr("Show a dialog to connect to an 'eric-ide' server")
+        )
+        self.connectServerAct.setWhatsThis(
+            self.tr(
+                """<b>Connect...</b>"""
+                """<p>This opens a dialog to enter the connection parameters to"""
+                """ connect to a remote 'eric-ide' server.</p>"""
+            )
+        )
+        self.connectServerAct.triggered.connect(self.__connectToServer)
+        self.actions.append(self.connectServerAct)
+
+        self.disconnectServerAct = EricAction(
+            self.tr("Disconnect"),
+            EricPixmapCache.getIcon("linkDisconnect"),
+            self.tr("Disconnect"),
+            QKeySequence(self.tr("Meta+Shift+D")),
+            0,
+            self,
+            "remote_server_disconnect",
+        )
+        self.disconnectServerAct.setStatusTip(
+            self.tr("Disconnect from the currently connected 'eric-ide' server")
+        )
+        self.disconnectServerAct.setWhatsThis(
+            self.tr(
+                """<b>Disconnect</b>"""
+                """<p>This disconnects from the currently connected 'eric-ide'"""
+                """ server.</p>"""
+            )
+        )
+        self.disconnectServerAct.triggered.connect(self.disconnectFromServer)
+        self.actions.append(self.disconnectServerAct)
+
+        self.stopServerAct = EricAction(
+            self.tr("Stop Server"),
+            EricPixmapCache.getIcon("stopScript"),
+            self.tr("Stop Server"),
+            QKeySequence(self.tr("Meta+Shift+S")),
+            0,
+            self,
+            "remote_server_shutdown",
+        )
+        self.stopServerAct.setStatusTip(
+            self.tr("Stop the currently connected 'eric-ide' server")
+        )
+        self.stopServerAct.setWhatsThis(
+            self.tr(
+                """<b>Stop Server</b>"""
+                """<p>This stops the currently connected 'eric-ide server.</p>"""
+            )
+        )
+        self.stopServerAct.triggered.connect(self.__shutdownServer)
+        self.actions.append(self.stopServerAct)
+
+        self.serverVersionsAct = EricAction(
+            self.tr("Show Server Versions"),
+            EricPixmapCache.getIcon("helpAbout"),
+            self.tr("Show Server Versions"),
+            0,
+            0,
+            self,
+            "remote_server_versions",
+        )
+        self.serverVersionsAct.setStatusTip(
+            self.tr("Show the eric-ide server versions")
+        )
+        self.serverVersionsAct.setWhatsThis(
+            self.tr(
+                """<b>Show Server Versions</b>"""
+                """<p>This opens a dialog to show the eric-ide server versions.</p>"""
+            )
+        )
+        self.serverVersionsAct.triggered.connect(self.serverVersions)
+        self.actions.append(self.serverVersionsAct)
+
+        self.disconnectServerAct.setEnabled(False)
+        self.stopServerAct.setEnabled(False)
+        self.serverVersionsAct.setEnabled(False)
+
+    def initMenu(self):
+        """
+        Public slot to initialize the eric-ide server menu.
+
+        @return the menu generated
+        @rtype QMenu
+        """
+        self.__serverProfilesMenu = QMenu(self.tr("Connect to"))
+        self.__serverProfilesMenu.aboutToShow.connect(self.__showServerProfilesMenu)
+        self.__serverProfilesMenu.triggered.connect(self.__serverProfileTriggered)
+
+        menu = QMenu(self.tr("eric-ide Server"), self.__ui)
+        menu.setTearOffEnabled(True)
+        menu.aboutToShow.connect(self.__showEricServerMenu)
+        menu.addAction(self.connectServerAct)
+        menu.addMenu(self.__serverProfilesMenu)
+        # TODO: add a 'Recent Connections' submenu
+        menu.addSeparator()
+        menu.addAction(self.disconnectServerAct)
+        menu.addSeparator()
+        menu.addAction(self.stopServerAct)
+        menu.addSeparator()
+        menu.addAction(self.serverVersionsAct)
+
+        self.__menus = {
+            "Main": menu,
+            ##"Recent": self.recentMenu,
+        }
+
+        return menu
+
+    def initToolbar(self, toolbarManager):
+        """
+        Public slot to initialize the eric-ide server toolbar.
+
+        @param toolbarManager reference to a toolbar manager object
+        @type EricToolBarManager
+        @return the toolbar generated
+        @rtype QToolBar
+        """
+        self.__connectButton = QToolButton()
+        self.__connectButton.setIcon(self.connectServerAct.icon())
+        self.__connectButton.setToolTip(self.connectServerAct.toolTip())
+        self.__connectButton.setWhatsThis(self.connectServerAct.whatsThis())
+        self.__connectButton.setPopupMode(
+            QToolButton.ToolButtonPopupMode.MenuButtonPopup
+        )
+        self.__connectButton.setMenu(self.__serverProfilesMenu)
+        self.connectServerAct.enabledChanged.connect(self.__connectButton.setEnabled)
+        self.__connectButton.clicked.connect(self.connectServerAct.triggered)
+
+        tb = QToolBar(self.tr("eric-ide Server"), self.__ui)
+        tb.setObjectName("EricServerToolbar")
+        tb.setToolTip(self.tr("eric-ide Server"))
+
+        act = tb.addWidget(self.__connectButton)
+        act.setText(self.connectServerAct.iconText())
+        act.setIcon(self.connectServerAct.icon())
+        tb.addAction(self.disconnectServerAct)
+        tb.addSeparator()
+        tb.addAction(self.stopServerAct)
+        tb.addSeparator()
+        tb.addAction(self.serverVersionsAct)
+
+        toolbarManager.addToolBar(tb, tb.windowTitle())
+
+        return tb
+
+    @pyqtSlot()
+    def __showEricServerMenu(self):
+        """
+        Private slot to display the server menu.
+        """
+        connected = self.isServerConnected()
+        self.connectServerAct.setEnabled(not connected)
+        self.disconnectServerAct.setEnabled(connected)
+        self.stopServerAct.setEnabled(connected)
+        self.serverVersionsAct.setEnabled(connected)
+
+        self.showMenu.emit("Main", self.__menus["Main"])
+
+    @pyqtSlot()
+    def __showServerProfilesMenu(self):
+        """
+        Private slot to prepare the eric server profiles menu.
+        """
+        profiles = Preferences.getEricServer("ConnectionProfiles")
+
+        self.__serverProfilesMenu.clear()
+
+        if not self.isServerConnected():
+            for profile in sorted(profiles):
+                act = self.__serverProfilesMenu.addAction(profile)
+                act.setData(profiles[profile])
+            self.__serverProfilesMenu.addSeparator()
+
+        self.__serverProfilesMenu.addAction(
+            self.tr("Manage Server Connections"), self.__manageServerProfiles
+        )
+
+    @pyqtSlot(bool)
+    def __connectionStateChanged(self, connected):
+        """
+        Private slot to handle the connection state change.
+
+        @param connected flag indicating the connection state
+        @type bool
+        """
+        if connected != self.__connected:  # prevent executing it twice in succession
+            self.__connected = connected
+
+            self.connectServerAct.setEnabled(not connected)
+            self.disconnectServerAct.setEnabled(connected)
+            self.stopServerAct.setEnabled(connected)
+            self.serverVersionsAct.setEnabled(connected)
+
+            if connected:
+                peerName = self.__connection.peerName()
+                EricMessageBox.information(
+                    None,
+                    self.tr("Connect to eric-ide Server"),
+                    self.tr(
+                        "<p>The eric-ide server at <b>{0}:{1}</b> was connected"
+                        " successfully.</p>"
+                    ).format(
+                        f"[{peerName}]" if ":" in peerName else peerName,
+                        self.__connection.peerPort(),
+                    ),
+                )
+            else:
+                EricMessageBox.information(
+                    None,
+                    self.tr("Disonnect from eric-ide Server"),
+                    self.tr("""The eric-ide server was disconnected."""),
+                )
+
+    @pyqtSlot()
+    def __connectToServer(self):
+        """
+        Private slot to connect to a remote eric-ide server.
+        """
+        from .EricServerConnectionDialog import EricServerConnectionDialog
+
+        dlg = EricServerConnectionDialog(parent=self.__ui)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            hostname, port, timeout = dlg.getData()
+            self.connectToServer(hostname, port=port, timeout=timeout)
+
+    @pyqtSlot()
+    def __shutdownServer(self):
+        """
+        Private slot to shut down the currently connected eric-ide server.
+        """
+        ok = EricMessageBox.yesNo(
+            None,
+            self.tr("Stop Server"),
+            self.tr(
+                "Do you really want to stop the currently connected eric-ide server?"
+                " No further connections will be possible without restarting the"
+                " server."
+            ),
+        )
+        if ok:
+            self.shutdownServer()
+
+    @pyqtSlot(QAction)
+    def __serverProfileTriggered(self, act):
+        """
+        Private slot to handle the selection of a remote server connection.
+
+        @param act reference to the triggered profile action
+        @type QAction
+        """
+        data = act.data()
+        if data is not None:
+            # handle the connection
+            hostname, port, timeout = data
+            self.connectToServer(hostname, port=port, timeout=timeout)
+
+    @pyqtSlot()
+    def __manageServerProfiles(self):
+        """
+        Private slot to show a dialog to manage the eric-ide server connection
+        profiles.
+        """
+        from .EricServerProfilesDialog import EricServerProfilesDialog
+
+        dlg = EricServerProfilesDialog(
+            Preferences.getEricServer("ConnectionProfiles"), self.__ui
+        )
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            profiles = dlg.getConnectionProfiles()
+            Preferences.setEricServer("ConnectionProfiles", profiles)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerProfilesDialog.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to manage server connection profiles.
+"""
+
+import copy
+
+from PyQt6.QtCore import Qt, pyqtSlot
+from PyQt6.QtWidgets import QDialog, QListWidgetItem
+
+from eric7.EricWidgets import EricMessageBox
+
+from .EricServerConnectionDialog import EricServerConnectionDialog
+from .Ui_EricServerProfilesDialog import Ui_EricServerProfilesDialog
+
+
+class EricServerProfilesDialog(QDialog, Ui_EricServerProfilesDialog):
+    """
+    Class implementing a dialog to manage server connection profiles.
+    """
+
+    def __init__(self, profiles, parent=None):
+        """
+        Constructor
+
+        @param profiles dictionary containing the server connection profiles
+        @type dict
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.__profiles = copy.deepcopy(profiles)
+        self.__populateProfilesList()
+
+        self.on_connectionsList_itemSelectionChanged()
+
+    def __populateProfilesList(self):
+        """
+        Private method to (re-) populate the list of server connection profiles.
+        """
+        self.connectionsList.clear()
+
+        for profile in self.__profiles:
+            itm = QListWidgetItem(profile, self.connectionsList)
+            itm.setData(Qt.ItemDataRole.UserRole, self.__profiles[profile])
+
+    def __getProfilesList(self):
+        """
+        Private method to get the list of defined profile names.
+
+        @return list of defined profile names
+        @rtype list of str
+        """
+        profileNames = []
+        for row in range(self.connectionsList.count()):
+            itm = self.connectionsList.item(row)
+            profileNames.append(itm.text())
+
+        return profileNames
+
+    @pyqtSlot()
+    def on_connectionsList_itemSelectionChanged(self):
+        """
+        Private slot to handle a change of selected items.
+        """
+        selectedItems = self.connectionsList.selectedItems()
+        self.editButton.setEnabled(len(selectedItems) == 1)
+        self.removeButton.setEnabled(len(selectedItems) > 0)
+
+    @pyqtSlot()
+    def on_addButton_clicked(self):
+        """
+        Private slot add a new connection profile.
+        """
+        dlg = EricServerConnectionDialog(
+            profileNames=self.__getProfilesList(), parent=self
+        )
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            profileData = dlg.getProfileData()
+            itm = QListWidgetItem(profileData[0], self.connectionsList)
+            itm.setData(Qt.ItemDataRole.UserRole, profileData[1:])
+
+    @pyqtSlot()
+    def on_editButton_clicked(self):
+        """
+        Private slot to edit the selected entry.
+        """
+        selectedItems = self.connectionsList.selectedItems()
+        if selectedItems:
+            itm = selectedItems[0]
+            dlg = EricServerConnectionDialog(
+                profileNames=self.__getProfilesList(), parent=self
+            )
+            data = itm.data(Qt.ItemDataRole.UserRole)
+            dlg.setProfileData(itm.text(), *data)
+            if dlg.exec() == QDialog.DialogCode.Accepted:
+                profileData = dlg.getProfileData()
+                itm.setText(profileData[0])
+                itm.setData(Qt.ItemDataRole.UserRole, profileData[1:])
+
+    @pyqtSlot()
+    def on_removeButton_clicked(self):
+        """
+        Private slot to remove the selected connection profiles.
+        """
+        yes = EricMessageBox.yesNo(
+            self,
+            self.tr("Remove Selected Entries"),
+            self.tr("Do you really want to remove the selected entries from the list?"),
+        )
+        if yes:
+            for itm in self.connectionsList.selectedItems()[:]:
+                self.connectionsList.takeItem(self.connectionsList.row(itm))
+                del itm
+
+    @pyqtSlot()
+    def on_resetButton_clicked(self):
+        """
+        Private slot to reset all changes performed.
+        """
+        yes = EricMessageBox.yesNo(
+            self,
+            self.tr("Reset Changes"),
+            self.tr(
+                "Do you really want to reset all changes performed up to this point?"
+            ),
+        )
+        if yes:
+            self.__populateProfilesList()
+
+    def getConnectionProfiles(self):
+        """
+        Public method to get the configured connection profiles.
+
+        @return dictionary containing the configured connection profiles
+        @rtype dict
+        """
+        profiles = {}
+
+        for row in range(self.connectionsList.count()):
+            itm = self.connectionsList.item(row)
+            profiles[itm.text()] = itm.data(Qt.ItemDataRole.UserRole)
+
+        return profiles
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerProfilesDialog.ui	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EricServerProfilesDialog</class>
+ <widget class="QDialog" name="EricServerProfilesDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>441</width>
+    <height>281</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Manage Server Connections</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QListWidget" name="connectionsList">
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::ExtendedSelection</enum>
+     </property>
+     <property name="sortingEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <layout class="QVBoxLayout" name="verticalLayout">
+     <item>
+      <widget class="QPushButton" name="addButton">
+       <property name="toolTip">
+        <string>Press to open a dialog to add a new server connection.</string>
+       </property>
+       <property name="text">
+        <string>Add...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="editButton">
+       <property name="toolTip">
+        <string>Press to open a dialog to edit the selected server connection.</string>
+       </property>
+       <property name="text">
+        <string>Edit...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="removeButton">
+       <property name="toolTip">
+        <string>Press to remove the selected server connections.</string>
+       </property>
+       <property name="text">
+        <string>Remove</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="verticalSpacer">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>20</width>
+         <height>40</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="resetButton">
+       <property name="toolTip">
+        <string>Press to reset all changes performed.</string>
+       </property>
+       <property name="text">
+        <string>Reset</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="1" column="0" colspan="2">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>EricServerProfilesDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>EricServerProfilesDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/__init__.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package implementing the components of the eric-ide remote server interface.
+"""
--- a/src/eric7/Sessions/SessionFile.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Sessions/SessionFile.py	Fri Feb 23 10:46:46 2024 +0100
@@ -16,6 +16,7 @@
 from eric7.EricGui.EricOverrideCursor import EricOverridenCursor
 from eric7.EricWidgets import EricMessageBox
 from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities
 
 
 class SessionFile(QObject):
@@ -46,6 +47,10 @@
         @rtype bool
         """
         # get references to objects we need
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
         project = ericApp().getObject("Project")
         projectBrowser = ericApp().getObject("ProjectBrowser")
         multiProject = ericApp().getObject("MultiProject")
@@ -223,13 +228,18 @@
 
         try:
             jsonString = json.dumps(sessionDict, indent=2) + "\n"
-            with open(filename, "w") as f:
-                f.write(jsonString)
+            if FileSystemUtilities.isRemoteFileName(filename):
+                title = self.tr("Save Remote Session")
+                fsInterface.writeFile(filename, jsonString.encode("utf-8"))
+            else:
+                title = self.tr("Save Session")
+                with open(filename, "w") as f:
+                    f.write(jsonString)
         except (OSError, TypeError) as err:
             with EricOverridenCursor():
                 EricMessageBox.critical(
                     None,
-                    self.tr("Save Session"),
+                    title,
                     self.tr(
                         "<p>The session file <b>{0}</b> could not be"
                         " written.</p><p>Reason: {1}</p>"
@@ -248,14 +258,23 @@
         @return flag indicating a successful read
         @rtype bool
         """
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
         try:
-            with open(filename, "r") as f:
-                jsonString = f.read()
+            if FileSystemUtilities.isRemoteFileName(filename):
+                title = self.tr("Read Remote Session")
+                jsonString = fsInterface.readFile(filename).decode("utf-8")
+            else:
+                title = self.tr("Read Session")
+                with open(filename, "r") as f:
+                    jsonString = f.read()
             sessionDict = json.loads(jsonString)
         except (OSError, json.JSONDecodeError) as err:
             EricMessageBox.critical(
                 None,
-                self.tr("Read Session"),
+                title,
                 self.tr(
                     "<p>The session file <b>{0}</b> could not be read.</p>"
                     "<p>Reason: {1}</p>"
--- a/src/eric7/SystemUtilities/FileSystemUtilities.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/SystemUtilities/FileSystemUtilities.py	Fri Feb 23 10:46:46 2024 +0100
@@ -358,14 +358,18 @@
     if f1 is None or f2 is None:
         return False
 
-    if followSymlinks:
-        if normcaseabspath(os.path.realpath(f1)) == normcaseabspath(
-            os.path.realpath(f2)
-        ):
-            return True
+    if isPlainFileName(f1) and isPlainFileName(f2):
+        if followSymlinks:
+            if normcaseabspath(os.path.realpath(f1)) == normcaseabspath(
+                os.path.realpath(f2)
+            ):
+                return True
+        else:
+            if normcaseabspath(f1) == normcaseabspath(f2):
+                return True
+
     else:
-        if normcaseabspath(f1) == normcaseabspath(f2):
-            return True
+        return f1 == f2
 
     return False
 
@@ -388,14 +392,20 @@
     if f1 is None or f2 is None:
         return False
 
-    if followSymlinks:
-        if normcaseabspath(os.path.dirname(os.path.realpath(f1))) == normcaseabspath(
-            os.path.dirname(os.path.realpath(f2))
-        ):
-            return True
+    if isPlainFileName(f1) and isPlainFileName(f2):
+        if followSymlinks:
+            if normcaseabspath(
+                os.path.dirname(os.path.realpath(f1))
+            ) == normcaseabspath(os.path.dirname(os.path.realpath(f2))):
+                return True
+        else:
+            if normcaseabspath(os.path.dirname(f1)) == normcaseabspath(
+                os.path.dirname(f2)
+            ):
+                return True
+
     else:
-        if normcaseabspath(os.path.dirname(f1)) == normcaseabspath(os.path.dirname(f2)):
-            return True
+        return os.path.dirname(f1) == os.path.dirname(f2)
 
     return False
 
@@ -491,30 +501,37 @@
     followsymlinks=True,
     checkStop=None,
     ignore=None,
+    recursive=True,
+    dirsonly=False,
 ):
     """
     Function returning a list of all files and directories.
 
     @param path root of the tree to check
     @type str
-    @param filesonly flag indicating that only files are wanted
-    @type bool
+    @param filesonly flag indicating that only files are wanted (defaults to False)
+    @type bool (optional)
     @param pattern a filename pattern or list of filename patterns to check
-        against
-    @type str or list of str
+        against (defaults to None)
+    @type str or list of str (optional)
     @param followsymlinks flag indicating whether symbolic links
-        should be followed
+        should be followed (defaults to True)
+    @type bool (optional)
+    @param checkStop function to be called to check for a stop (defaults to None)
+    @type function (optional)
+    @param ignore list of entries to be ignored (defaults to None)
+    @type list of str (optional)
+    @param recursive flag indicating a recursive search (defaults to True)
+    @type bool (optional)
+    @param dirsonly flag indicating to return only directories. When True it has
+        precedence over the 'filesonly' parameter ((defaults to False)
     @type bool
-    @param checkStop function to be called to check for a stop
-    @type function
-    @param ignore list of entries to be ignored
-    @type list of str
     @return list of all files and directories in the tree rooted
         at path. The names are expanded to start with path.
     @rtype list of str
     """
     patterns = pattern if isinstance(pattern, list) else [pattern]
-    files = [] if filesonly else [path]
+    files = [] if (filesonly and not dirsonly) else [path]
     ignoreList = [
         ".svn",
         ".hg",
@@ -546,18 +563,21 @@
                 continue
 
             if dirEntry.is_dir():
-                if dirEntry.path in ignoreList:
-                    continue
-                if dirEntry.is_symlink() and not followsymlinks:
+                if dirEntry.path in ignoreList or (
+                    dirEntry.is_symlink() and not followsymlinks
+                ):
                     continue
-                files += direntries(
-                    dirEntry.path,
-                    filesonly=filesonly,
-                    pattern=pattern,
-                    followsymlinks=followsymlinks,
-                    checkStop=checkStop,
-                    ignore=ignore,
-                )
+                if recursive:
+                    files += direntries(
+                        dirEntry.path,
+                        filesonly=filesonly,
+                        pattern=pattern,
+                        followsymlinks=followsymlinks,
+                        checkStop=checkStop,
+                        ignore=ignore,
+                    )
+                elif dirsonly:
+                    files.append(dirEntry.path)
             else:
                 files.append(dirEntry.path)
     return files
--- a/src/eric7/Tasks/TasksFile.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Tasks/TasksFile.py	Fri Feb 23 10:46:46 2024 +0100
@@ -16,6 +16,7 @@
 from eric7.EricGui.EricOverrideCursor import EricOverridenCursor
 from eric7.EricWidgets import EricMessageBox
 from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities
 
 from .Task import TaskPriority, TaskType
 
@@ -46,6 +47,10 @@
         @return flag indicating a successful write
         @rtype bool
         """
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
         # prepare the tasks data dictionary
         # step 0: header
         tasksDict = {}
@@ -85,13 +90,18 @@
 
         try:
             jsonString = json.dumps(tasksDict, indent=2) + "\n"
-            with open(filename, "w") as f:
-                f.write(jsonString)
+            if FileSystemUtilities.isRemoteFileName(filename):
+                title = self.tr("Save Remote Tasks")
+                fsInterface.writeFile(filename, jsonString.encode("utf-8"))
+            else:
+                title = self.tr("Save Tasks")
+                with open(filename, "w") as f:
+                    f.write(jsonString)
         except (OSError, TypeError) as err:
             with EricOverridenCursor():
                 EricMessageBox.critical(
                     None,
-                    self.tr("Save Tasks"),
+                    title,
                     self.tr(
                         "<p>The tasks file <b>{0}</b> could not be"
                         " written.</p><p>Reason: {1}</p>"
@@ -110,14 +120,23 @@
         @return flag indicating a successful read
         @rtype bool
         """
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+
         try:
-            with open(filename, "r") as f:
-                jsonString = f.read()
+            if FileSystemUtilities.isRemoteFileName(filename):
+                title = self.tr("Read Remote Tasks")
+                jsonString = fsInterface.readFile(filename).decode("utf-8")
+            else:
+                title = self.tr("Read Tasks")
+                with open(filename, "r") as f:
+                    jsonString = f.read()
             tasksDict = json.loads(jsonString)
         except (OSError, json.JSONDecodeError) as err:
             EricMessageBox.critical(
                 None,
-                self.tr("Read Tasks"),
+                title,
                 self.tr(
                     "<p>The tasks file <b>{0}</b> could not be read.</p>"
                     "<p>Reason: {1}</p>"
--- a/src/eric7/UI/Browser.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/UI/Browser.py	Fri Feb 23 10:46:46 2024 +0100
@@ -35,6 +35,7 @@
 from eric7.EricWidgets import EricFileDialog, EricMessageBox
 from eric7.EricWidgets.EricApplication import ericApp
 from eric7.Project.ProjectBrowserModel import ProjectBrowserSimpleDirectoryItem
+from eric7.RemoteServerInterface import EricServerFileDialog
 from eric7.SystemUtilities import FileSystemUtilities
 from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog
 from eric7.Utilities import MimeTypes
@@ -106,10 +107,12 @@
     pdfFile = pyqtSignal(str)
     testFile = pyqtSignal(str)
 
-    def __init__(self, parent=None):
+    def __init__(self, serverInterface, parent=None):
         """
         Constructor
 
+        @param serverInterface reference to the 'eric-ide' server interface object
+        @type EricServerInterface
         @param parent parent widget
         @type QWidget
         """
@@ -118,7 +121,10 @@
         self.setWindowTitle(QCoreApplication.translate("Browser", "File-Browser"))
         self.setWindowIcon(EricPixmapCache.getIcon("eric"))
 
-        self.__model = BrowserModel()
+        self.__ericServerInterface = serverInterface
+        self.__remotefsInterface = serverInterface.getServiceInterface("FileSystem")
+
+        self.__model = BrowserModel(fsInterface=self.__remotefsInterface)
         self.__sortModel = BrowserSortFilterProxyModel()
         self.__sortModel.setSourceModel(self.__model)
         self.setModel(self.__sortModel)
@@ -338,15 +344,19 @@
         # create the directory menu
         self.dirMenu = QMenu(self)
         self.dirMenu.addAction(
-            QCoreApplication.translate("Browser", "New toplevel directory..."),
-            self.__newToplevelDir,
+            QCoreApplication.translate("Browser", "New Top Level Directory..."),
+            self.__newTopLevelDir,
+        )
+        self.__dmRemoteTopLevelAct = self.dirMenu.addAction(
+            QCoreApplication.translate("Browser", "New Remote Top Level Directory..."),
+            self.__newRemoteTopLevelDir,
         )
         self.addAsTopLevelAct = self.dirMenu.addAction(
-            QCoreApplication.translate("Browser", "Add as toplevel directory"),
+            QCoreApplication.translate("Browser", "Add as top level directory"),
             self.__addAsToplevelDir,
         )
         self.removeFromToplevelAct = self.dirMenu.addAction(
-            QCoreApplication.translate("Browser", "Remove from toplevel"),
+            QCoreApplication.translate("Browser", "Remove from top level"),
             self.__removeToplevel,
         )
         self.dirMenu.addSeparator()
@@ -387,8 +397,12 @@
 
         self.attributeMenu = QMenu(self)
         self.attributeMenu.addAction(
-            QCoreApplication.translate("Browser", "New toplevel directory..."),
-            self.__newToplevelDir,
+            QCoreApplication.translate("Browser", "New Top Level Directory..."),
+            self.__newTopLevelDir,
+        )
+        self.__amRemoteTopLevelAct = self.attributeMenu.addAction(
+            QCoreApplication.translate("Browser", "New Remote Top Level Directory..."),
+            self.__newRemoteTopLevelDir,
         )
         self.attributeMenu.addSeparator()
         self.attributeMenu.addMenu(self.gotoMenu)
@@ -396,8 +410,12 @@
         # create the background menu
         self.backMenu = QMenu(self)
         self.backMenu.addAction(
-            QCoreApplication.translate("Browser", "New toplevel directory..."),
-            self.__newToplevelDir,
+            QCoreApplication.translate("Browser", "New Top Level Directory..."),
+            self.__newTopLevelDir,
+        )
+        self.__bmRemoteTopLevelAct = self.backMenu.addAction(
+            QCoreApplication.translate("Browser", "New Remote Top Level Directory..."),
+            self.__newRemoteTopLevelDir,
         )
         self.backMenu.addSeparator()
         self.backMenu.addAction(self.showHiddenFilesAct)
@@ -431,7 +449,7 @@
 
     def _contextMenuRequested(self, coord):
         """
-        Protected slot to show the context menu of the listview.
+        Protected slot to show the context menu of the list view.
 
         @param coord the position of the mouse pointer
         @type QPoint
@@ -475,6 +493,9 @@
                     self.openInPdfViewerAct.setVisible(False)
                     self.menu.popup(coord)
                 elif isinstance(itm, BrowserClassAttributeItem):
+                    self.__amRemoteTopLevelAct.setEnabled(
+                        self.__ericServerInterface.isServerConnected()
+                    )
                     self.attributeMenu.popup(coord)
                 elif isinstance(itm, BrowserDirectoryItem):
                     if not index.parent().isValid():
@@ -483,10 +504,19 @@
                     else:
                         self.removeFromToplevelAct.setEnabled(False)
                         self.addAsTopLevelAct.setEnabled(True)
+                    self.__dmRemoteTopLevelAct.setEnabled(
+                        self.__ericServerInterface.isServerConnected()
+                    )
                     self.dirMenu.popup(coord)
                 else:
+                    self.__bmRemoteTopLevelAct.setEnabled(
+                        self.__ericServerInterface.isServerConnected()
+                    )
                     self.backMenu.popup(coord)
             else:
+                self.__bmRemoteTopLevelAct.setEnabled(
+                    self.__ericServerInterface.isServerConnected()
+                )
                 self.backMenu.popup(self.mapToGlobal(coord))
 
     def _showGotoMenu(self):
@@ -755,6 +785,7 @@
         # remember the current state
         Preferences.setUI("BrowsersListHiddenFiles", checked)
 
+    @pyqtSlot()
     def handleTesting(self):
         """
         Public slot to handle the testing popup menu entry.
@@ -769,13 +800,14 @@
         if pyfn is not None:
             self.testFile.emit(pyfn)
 
-    def __newToplevelDir(self):
+    @pyqtSlot()
+    def __newTopLevelDir(self):
         """
-        Private slot to handle the New toplevel directory popup menu entry.
+        Private slot to handle the New Top Level Directory popup menu entry.
         """
         dname = EricFileDialog.getExistingDirectory(
             None,
-            QCoreApplication.translate("Browser", "New toplevel directory"),
+            QCoreApplication.translate("Browser", "New Top Level Directory"),
             "",
             EricFileDialog.ShowDirsOnly,
         )
@@ -783,22 +815,39 @@
             dname = os.path.abspath(FileSystemUtilities.toNativeSeparators(dname))
             self.__model.addTopLevelDir(dname)
 
+    @pyqtSlot()
+    def __newRemoteTopLevelDir(self):
+        """
+        Private slot to handle the New Remote Top Level Directory popup menu entry.
+        """
+        dname = EricServerFileDialog.getExistingDirectory(
+            None,
+            QCoreApplication.translate("Browser", "New Remote Top Level Directory"),
+            "",
+            dirsOnly=True,
+        )
+        if dname:
+            self.__model.addTopLevelDir(dname)
+
+    @pyqtSlot()
     def __removeToplevel(self):
         """
-        Private slot to handle the Remove from toplevel popup menu entry.
+        Private slot to handle the Remove from top level popup menu entry.
         """
         index = self.currentIndex()
         sindex = self.model().mapToSource(index)
         self.__model.removeToplevelDir(sindex)
 
+    @pyqtSlot()
     def __addAsToplevelDir(self):
         """
-        Private slot to handle the Add as toplevel directory popup menu entry.
+        Private slot to handle the Add as top level directory popup menu entry.
         """
         index = self.currentIndex()
         dname = self.model().item(index).dirName()
         self.__model.addTopLevelDir(dname)
 
+    @pyqtSlot()
     def __refreshDirectory(self):
         """
         Private slot to refresh a directory entry.
@@ -807,6 +856,7 @@
         refreshDir = self.model().item(index).dirName()
         self.__model.directoryChanged(refreshDir)
 
+    @pyqtSlot()
     def __findInDirectory(self):
         """
         Private slot to handle the Find in directory popup menu entry.
@@ -816,6 +866,7 @@
 
         ericApp().getObject("UserInterface").showFindFilesWidget(searchDir=searchDir)
 
+    @pyqtSlot()
     def __replaceInDirectory(self):
         """
         Private slot to handle the Find&Replace in directory popup menu entry.
@@ -825,6 +876,7 @@
 
         ericApp().getObject("UserInterface").showReplaceFilesWidget(searchDir=searchDir)
 
+    @pyqtSlot(str)
     def handleProgramChange(self, fn):
         """
         Public slot to handle the programChange signal.
@@ -834,6 +886,7 @@
         """
         self.__model.programChange(os.path.dirname(fn))
 
+    @pyqtSlot(str)
     def handleInterpreterChanged(self, interpreter):
         """
         Public slot to handle a change of the debug client's interpreter.
--- a/src/eric7/UI/BrowserModel.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/UI/BrowserModel.py	Fri Feb 23 10:46:46 2024 +0100
@@ -62,14 +62,17 @@
     Class implementing the browser model.
     """
 
-    def __init__(self, parent=None, nopopulate=False):
+    def __init__(self, parent=None, nopopulate=False, fsInterface=None):
         """
         Constructor
 
-        @param parent reference to parent object
-        @type QObject
-        @param nopopulate flag indicating to not populate the model
-        @type bool
+        @param parent reference to parent object (defaults to None)
+        @type QObject (optional)
+        @param nopopulate flag indicating to not populate the model (defaults to False)
+        @type bool (optional)
+        @param fsInterface reference to the 'eric-ide' server interface object
+            (defaults to None)
+        @type EricServerFileSystemInterface (optional)
         """
         super().__init__(parent)
 
@@ -78,6 +81,8 @@
         self.__sysPathInterpreter = ""
         self.__sysPathItem = None
 
+        self.__remotefsInterface = fsInterface
+
         if not nopopulate:
             self.watchedItems = {}
             self.watchedFileItems = {}
@@ -326,8 +331,8 @@
             dirName = itm.dirName()
             if (
                 dirName != ""
-                and not dirName.startswith("//")
-                and not dirName.startswith("\\\\")
+                and not FileSystemUtilities.isRemoteFileName(dirName)
+                and not dirName.startswith(("//", "\\\\"))
             ):
                 if dirName not in self.watcher.directories():
                     self.watcher.addPath(dirName)
@@ -442,7 +447,9 @@
                 )
 
         for d in self.toplevelDirs:
-            itm = BrowserDirectoryItem(self.rootItem, d)
+            itm = BrowserDirectoryItem(
+                self.rootItem, d, fsInterface=self.__remotefsInterface
+            )
             self._addItem(itm, self.rootItem)
 
     def interpreterChanged(self, interpreter):
@@ -502,7 +509,9 @@
             self.endRemoveRows()
             self.progDir = None
 
-        itm = BrowserDirectoryItem(self.rootItem, dirname)
+        itm = BrowserDirectoryItem(
+            self.rootItem, dirname, fsInterface=self.__remotefsInterface
+        )
         self.addItem(itm)
         self.progDir = itm
 
@@ -514,7 +523,9 @@
         @type str
         """
         if dirname not in self.toplevelDirs:
-            itm = BrowserDirectoryItem(self.rootItem, dirname)
+            itm = BrowserDirectoryItem(
+                self.rootItem, dirname, fsInterface=self.__remotefsInterface
+            )
             self.addItem(itm)
             self.toplevelDirs.append(itm.dirName())
 
@@ -607,39 +618,85 @@
         """
         self._addWatchedItem(parentItem)
 
-        qdir = QDir(parentItem.dirName())
+        dirName = parentItem.dirName()
+        if FileSystemUtilities.isPlainFileName(dirName):
+            qdir = QDir(dirName)
 
-        dirFilter = (
-            QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot | QDir.Filter.Hidden
-        )
-        entryInfoList = qdir.entryInfoList(dirFilter)
-        if len(entryInfoList) > 0:
-            if repopulate:
-                self.beginInsertRows(
-                    self.createIndex(parentItem.row(), 0, parentItem),
-                    0,
-                    len(entryInfoList) - 1,
-                )
-            for f in entryInfoList:
-                if f.isDir():
-                    node = BrowserDirectoryItem(
-                        parentItem,
-                        FileSystemUtilities.toNativeSeparators(f.absoluteFilePath()),
-                        False,
+            dirFilter = (
+                QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot | QDir.Filter.Hidden
+            )
+            entryInfoList = qdir.entryInfoList(dirFilter)
+            if len(entryInfoList) > 0:
+                if repopulate:
+                    self.beginInsertRows(
+                        self.createIndex(parentItem.row(), 0, parentItem),
+                        0,
+                        len(entryInfoList) - 1,
                     )
-                else:
-                    fileFilters = Preferences.getUI("BrowsersFileFilters").split(";")
-                    if fileFilters:
-                        fn = f.fileName()
-                        if any(fnmatch.fnmatch(fn, ff.strip()) for ff in fileFilters):
-                            continue
-                    node = BrowserFileItem(
-                        parentItem,
-                        FileSystemUtilities.toNativeSeparators(f.absoluteFilePath()),
+                for f in entryInfoList:
+                    if f.isDir():
+                        node = BrowserDirectoryItem(
+                            parentItem,
+                            FileSystemUtilities.toNativeSeparators(
+                                f.absoluteFilePath()
+                            ),
+                            False,
+                        )
+                    else:
+                        fileFilters = Preferences.getUI("BrowsersFileFilters").split(
+                            ";"
+                        )
+                        if fileFilters:
+                            fn = f.fileName()
+                            if any(
+                                fnmatch.fnmatch(fn, ff.strip()) for ff in fileFilters
+                            ):
+                                continue
+                        node = BrowserFileItem(
+                            parentItem,
+                            FileSystemUtilities.toNativeSeparators(
+                                f.absoluteFilePath()
+                            ),
+                        )
+                    self._addItem(node, parentItem)
+                if repopulate:
+                    self.endInsertRows()
+
+        elif FileSystemUtilities.isRemoteFileName(dirName):
+            entriesList = self.__remotefsInterface.listdir(dirName)[2]
+            if len(entriesList) > 0:
+                if repopulate:
+                    self.beginInsertRows(
+                        self.createIndex(parentItem.row(), 0, parentItem),
+                        0,
+                        len(entryInfoList) - 1,
                     )
-                self._addItem(node, parentItem)
-            if repopulate:
-                self.endInsertRows()
+                for entry in entriesList:
+                    if entry["is_dir"]:
+                        node = BrowserDirectoryItem(
+                            parentItem,
+                            FileSystemUtilities.remoteFileName(entry["path"]),
+                            False,
+                            fsInterface=self.__remotefsInterface,
+                        )
+                    else:
+                        fileFilters = Preferences.getUI("BrowsersFileFilters").split(
+                            ";"
+                        )
+                        if fileFilters:
+                            fn = entry["name"]
+                            if any(
+                                fnmatch.fnmatch(fn, ff.strip()) for ff in fileFilters
+                            ):
+                                continue
+                        node = BrowserFileItem(
+                            parentItem,
+                            FileSystemUtilities.remoteFileName(entry["path"]),
+                            fsInterface=self.__remotefsInterface,
+                        )
+                    self._addItem(node, parentItem)
+                if repopulate:
+                    self.endInsertRows()
 
     def populateSysPathItem(self, parentItem, repopulate=False):
         """
@@ -692,6 +749,7 @@
         """
         from eric7.Utilities import ClassBrowsers
 
+        # TODO: add support for 'remote:' directories
         moduleName = parentItem.moduleName()
         fileName = parentItem.fileName()
         try:
@@ -1152,6 +1210,7 @@
     Class implementing the data structure for browser simple directory items.
     """
 
+    # TODO: add support for 'remote:' directories
     def __init__(self, parent, text, path=""):
         """
         Constructor
@@ -1186,6 +1245,7 @@
         @param full flag indicating full path name should be displayed
         @type bool
         """
+        # TODO: add support for 'remote:' directories
         self._dirName = os.path.abspath(dinfo)
         self.itemData[0] = os.path.basename(self._dirName)
 
@@ -1233,7 +1293,7 @@
     Class implementing the data structure for browser directory items.
     """
 
-    def __init__(self, parent, dinfo, full=True):
+    def __init__(self, parent, dinfo, full=True, fsInterface=None):
         """
         Constructor
 
@@ -1241,21 +1301,33 @@
         @type BrowserItem
         @param dinfo dinfo is the string for the directory
         @type str
-        @param full flag indicating full pathname should be displayed
-        @type bool
+        @param full flag indicating full pathname should be displayed (defaults to True)
+        @type bool (optional)
+        @param fsInterface reference to the 'eric-ide' server file system interface
+            (defaults to None)
+        @type EricServerFileSystemInterface (optional)
         """
-        self._dirName = os.path.abspath(dinfo)
-        dn = self._dirName if full else os.path.basename(self._dirName)
+        self.__fsInterface = fsInterface
+
+        if FileSystemUtilities.isRemoteFileName(dinfo):
+            self._dirName = dinfo
+            dn = self._dirName if full else self.__fsInterface.basename(self._dirName)
+        else:
+            self._dirName = os.path.abspath(dinfo)
+            dn = self._dirName if full else os.path.basename(self._dirName)
         BrowserItem.__init__(self, parent, dn)
 
         self.type_ = BrowserItemType.Directory
         if (
-            not FileSystemUtilities.isDrive(self._dirName)
+            FileSystemUtilities.isPlainFileName(self._dirName)
+            and not FileSystemUtilities.isDrive(self._dirName)
             and os.path.lexists(self._dirName)
             and os.path.islink(self._dirName)
         ):
             self.symlink = True
             self.icon = EricPixmapCache.getSymlinkIcon("dirClosed")
+        elif FileSystemUtilities.isRemoteFileName(self._dirName):
+            self.icon = EricPixmapCache.getIcon("open-remote")
         else:
             self.icon = EricPixmapCache.getIcon("dirClosed")
         self._populated = False
@@ -1270,8 +1342,12 @@
         @param full flag indicating full pathname should be displayed
         @type bool
         """
-        self._dirName = os.path.abspath(dinfo)
-        dn = self._dirName if full else os.path.basename(self._dirName)
+        if FileSystemUtilities.isRemoteFileName(dinfo):
+            self._dirName = dinfo
+            dn = self._dirName if full else self.__fsInterface.basename(self._dirName)
+        else:
+            self._dirName = os.path.abspath(dinfo)
+            dn = self._dirName if full else os.path.basename(self._dirName)
         self.itemData[0] = dn
 
     def dirName(self):
@@ -1342,12 +1418,13 @@
         return "sys.path"
 
 
+# TODO: add support for 'remote:' directories
 class BrowserFileItem(BrowserItem):
     """
     Class implementing the data structure for browser file items.
     """
 
-    def __init__(self, parent, finfo, full=True, sourceLanguage=""):
+    def __init__(self, parent, finfo, full=True, sourceLanguage="", fsInterface=None):
         """
         Constructor
 
@@ -1355,17 +1432,29 @@
         @type BrowserItem
         @param finfo the string for the file
         @type str
-        @param full flag indicating full pathname should be displayed
-        @type bool
-        @param sourceLanguage source code language of the project
-        @type str
+        @param full flag indicating full pathname should be displayed (defaults to True)
+        @type bool (optional)
+        @param sourceLanguage source code language of the project (defaults to "")
+        @type str (optional)
+        @param fsInterface reference to the 'eric-ide' server file system interface
+            (defaults to None)
+        @type EricServerFileSystemInterface (optional)
         """
-        BrowserItem.__init__(self, parent, os.path.basename(finfo))
+        self.__fsInterface = fsInterface
 
+        if FileSystemUtilities.isRemoteFileName(finfo):
+            dirname, basename = self.__fsInterface.split(finfo)
+            self.fileext = self.__fsInterface.splitext(finfo)[1].lower()
+            self._filename = finfo
+        else:
+            dirname, basename = os.path.split(finfo)
+            self.fileext = os.path.splitext(finfo)[1].lower()
+            self._filename = os.path.abspath(finfo)
+
+        BrowserItem.__init__(self, parent, basename)
+
+        self._dirName = dirname
         self.type_ = BrowserItemType.File
-        self.fileext = os.path.splitext(finfo)[1].lower()
-        self._filename = os.path.abspath(finfo)
-        self._dirName = os.path.dirname(finfo)
         self.sourceLanguage = sourceLanguage
 
         self._moduleName = ""
@@ -1375,17 +1464,17 @@
             pixName = "filePython"
             self._populated = False
             self._lazyPopulation = True
-            self._moduleName = os.path.basename(finfo)
+            self._moduleName = basename
         elif self.isCythonFile():
             pixName = "lexerCython"
             self._populated = False
             self._lazyPopulation = True
-            self._moduleName = os.path.basename(finfo)
+            self._moduleName = basename
         elif self.isRubyFile():
             pixName = "fileRuby"
             self._populated = False
             self._lazyPopulation = True
-            self._moduleName = os.path.basename(finfo)
+            self._moduleName = basename
         elif self.isDesignerFile():
             pixName = "fileDesigner"
         elif self.isLinguistFile():
@@ -1409,18 +1498,22 @@
             pixName = "fileJavascript"
             self._populated = False
             self._lazyPopulation = True
-            self._moduleName = os.path.basename(finfo)
+            self._moduleName = basename
         elif self.isEricGraphicsFile():
             pixName = "fileUML"
         elif self.isParsableFile():
             pixName = ClassBrowsers.getIcon(self._filename)
             self._populated = False
             self._lazyPopulation = True
-            self._moduleName = os.path.basename(finfo)
+            self._moduleName = basename
         else:
             pixName = "fileMisc"
 
-        if os.path.lexists(self._filename) and os.path.islink(self._filename):
+        if (
+            FileSystemUtilities.isPlainFileName(self._filename)
+            and os.path.lexists(self._filename)
+            and os.path.islink(self._filename)
+        ):
             self.symlink = True
             self.icon = EricPixmapCache.getSymlinkIcon(pixName)
         else:
@@ -1435,12 +1528,25 @@
         @param full flag indicating full pathname should be displayed
         @type bool
         """
-        self._filename = os.path.abspath(finfo)
-        self.itemData[0] = os.path.basename(finfo)
-        self.fileext = os.path.splitext(finfo)[1].lower()
-        if self.isPython3File() or self.isRubyFile() or self.isParsableFile():
-            self._dirName = os.path.dirname(finfo)
-            self._moduleName = os.path.basename(finfo)
+        if FileSystemUtilities.isRemoteFileName(finfo):
+            dirname, basename = self.__fsInterface.split(finfo)
+            self.fileext = self.__fsInterface.splitext(finfo)[1].lower()
+            self._filename = finfo
+        else:
+            dirname, basename = os.path.split(finfo)
+            self.fileext = os.path.splitext(finfo)[1].lower()
+            self._filename = os.path.abspath(finfo)
+
+        self.itemData[0] = basename
+        if (
+            self.isPython3File()
+            or self.isCythonFile()
+            or self.isRubyFile()
+            or self.isJavaScriptFile()
+            or self.isParsableFile()
+        ):
+            self._dirName = dirname
+            self._moduleName = basename
 
     def fileName(self):
         """
@@ -1654,8 +1760,18 @@
             return order == Qt.SortOrder.DescendingOrder
 
         if issubclass(other.__class__, BrowserFileItem):
-            sinit = os.path.basename(self._filename).startswith("__init__.py")
-            oinit = os.path.basename(other.fileName()).startswith("__init__.py")
+            if FileSystemUtilities.isRemoteFileName(self._filename):
+                basename = self.__fsInterface.basename(self._filename)
+            else:
+                basename = os.path.basename(self._filename)
+            sinit = basename.startswith("__init__.py")
+
+            if FileSystemUtilities.isRemoteFileName(other.fileName()):
+                basename = self.__fsInterface.basename(other.fileName())
+            else:
+                basename = os.path.basename(other.fileName())
+            oinit = basename.startswith("__init__.py")
+
             if sinit and not oinit:
                 return order == Qt.SortOrder.AscendingOrder
             if not sinit and oinit:
--- a/src/eric7/UI/UserInterface.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/UI/UserInterface.py	Fri Feb 23 10:46:46 2024 +0100
@@ -79,6 +79,7 @@
 from eric7.Preferences import Shortcuts
 from eric7.Project.Project import Project
 from eric7.QScintilla.SpellChecker import SpellChecker
+from eric7.RemoteServerInterface.EricServerInterface import EricServerInterface
 from eric7.Sessions.SessionFile import SessionFile
 from eric7.SystemUtilities import (
     DesktopUtilities,
@@ -306,6 +307,10 @@
 
         splash.showMessage(self.tr("Initializing Basic Services..."))
 
+        # create the remote server interface
+        logging.debug("Creating 'eric-ide' Server Interface...")
+        self.__ericServerInterface = EricServerInterface(self)
+
         # Generate the conda interface
         logging.debug("Creating Conda Interface...")
         self.condaInterface = Conda(self)
@@ -324,7 +329,7 @@
 
         # Generate an empty project object and multi project object
         logging.debug("Creating Project Manager...")
-        self.project = Project(self)
+        self.project = Project(self, remoteServer=self.__ericServerInterface)
         ericApp().registerObject("Project", self.project)
 
         logging.debug("Creating Multi-Project Manager...")
@@ -586,6 +591,19 @@
                 self.viewmanager.closeDeviceEditors
             )
 
+        self.__ericServerInterface.connectionStateChanged.connect(
+            self.viewmanager.remoteConnectionChanged
+        )
+        self.__ericServerInterface.connectionStateChanged.connect(
+            self.shell.remoteConnectionChanged
+        )
+        self.__ericServerInterface.connectionStateChanged.connect(
+            self.project.remoteConnectionChanged
+        )
+        self.__ericServerInterface.aboutToDisconnect.connect(
+            self.viewmanager.closeRemoteEditors
+        )
+
         # create the toolbar manager object
         self.toolbarManager = EricToolBarManager(self, self)
         self.toolbarManager.setMainWindow(self)
@@ -631,6 +649,7 @@
             ericApp().registerObject("MicroPython", self.microPythonWidget)
         ericApp().registerObject("JediAssistant", self.jediAssistant)
         ericApp().registerObject("PluginRepositoryViewer", self.pluginRepositoryViewer)
+        ericApp().registerObject("EricServer", self.__ericServerInterface)
 
         # create the various JSON file interfaces
         self.__sessionFile = SessionFile(True)
@@ -791,7 +810,7 @@
         # Create the view manager depending on the configuration setting
         logging.debug("Creating Viewmanager...")
         self.viewmanager = ViewManager.factory(
-            self, self.__debugServer, self.pluginManager
+            self, self.__debugServer, self.__ericServerInterface, self.pluginManager
         )
 
         # Create previewer
@@ -848,7 +867,7 @@
             logging.debug("Creating File Browser...")
             from .Browser import Browser  # noqa: I101
 
-            self.browser = Browser()
+            self.browser = Browser(self.__ericServerInterface)
         else:
             logging.debug("File Browser disabled")
             self.browser = None
@@ -3568,6 +3587,9 @@
         # initialize multi project actions
         self.multiProject.initActions()
 
+        # initialize eric-ide server actions
+        self.__ericServerInterface.initActions()
+
     def __initQtDocActions(self):
         """
         Private slot to initialize the action to show the Qt documentation.
@@ -3777,6 +3799,12 @@
             mb.setNativeMenuBar(False)
 
         ##############################################################
+        ## Remote Server menu
+        ##############################################################
+
+        self.__menus["server"] = self.__ericServerInterface.initMenu()
+
+        ##############################################################
         ## File menu
         ##############################################################
 
@@ -3791,6 +3819,9 @@
         act = self.__menus["file"].actions()[0]
         sep = self.__menus["file"].insertSeparator(act)
         self.__menus["file"].insertAction(sep, self.newWindowAct)
+        self.__menus["file"].insertSeparator(sep)
+        self.__menus["file"].insertMenu(sep, self.__menus["server"])
+        self.__menus["file"].insertSeparator(sep)
         self.__menus["file"].aboutToShow.connect(self.__showFileMenu)
 
         ##############################################################
@@ -4088,6 +4119,7 @@
         helptb = QToolBar(self.tr("Help"), self)
         profilestb = QToolBar(self.tr("Profiles"), self)
         pluginstb = QToolBar(self.tr("Plugins"), self)
+        servertb = self.__ericServerInterface.initToolbar(self.toolbarManager)
 
         toolstb.setObjectName("ToolsToolbar")
         testingtb.setObjectName("UnittestToolbar")
@@ -4190,6 +4222,7 @@
 
         # add the various toolbars
         self.addToolBar(filetb)
+        self.addToolBar(servertb)
         self.addToolBar(edittb)
         self.addToolBar(searchtb)
         self.addToolBar(viewtb)
@@ -4240,6 +4273,7 @@
         ]
         self.__toolbars["spelling"] = [spellingtb.windowTitle(), spellingtb, ""]
         self.__toolbars["vcs"] = [vcstb.windowTitle(), vcstb, "vcs"]
+        self.__toolbars["server"] = [servertb.windowTitle(), servertb, ""]
 
     def __initDebugToolbarsLayout(self):
         """
@@ -8587,3 +8621,18 @@
         elif self.__layoutType == "Sidebars":
             self.__activateLeftRightSidebarWidget(self.__virtualenvManagerWidget)
         self.__virtualenvManagerWidget.setFocus(Qt.FocusReason.ActiveWindowFocusReason)
+
+    ############################################################
+    ## Interface to the eric-ide server interface
+    ############################################################
+
+    def isEricServerConnected(self):
+        """
+        Public method to check, if a connection to an eric-ide server has been
+        established.
+
+        @return flag indicating the interface connection state
+        @rtype bool
+        """
+        # simply delegated to the eric-ide server interface object
+        return self.__ericServerInterface.isServerConnected()
--- a/src/eric7/Utilities/ClassBrowsers/__init__.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Utilities/ClassBrowsers/__init__.py	Fri Feb 23 10:46:46 2024 +0100
@@ -108,7 +108,7 @@
     return None
 
 
-def readmodule(module, path=None, isPyFile=False):
+def readmodule(module, searchPath=None, isPyFile=False):
     """
     Function to read a source file and return a dictionary of classes, functions,
     modules, etc. .
@@ -118,7 +118,7 @@
 
     @param module name of the source file
     @type str
-    @param path list of paths the file should be searched in
+    @param searchPath list of paths the file should be searched in
     @type list of str
     @param isPyFile flag indicating a Python file
     @type bool
@@ -126,13 +126,13 @@
     @rtype dict
     """
     ext = os.path.splitext(module)[1].lower()
-    path = [] if path is None else path[:]
+    searchPath = [] if searchPath is None else searchPath[:]
 
     if not isPyFile:
         for classBrowserName in ClassBrowserRegistry:
             if ext in ClassBrowserRegistry[classBrowserName]["Extensions"]:
                 return ClassBrowserRegistry[classBrowserName]["ReadModule"](
-                    module, path
+                    module, searchPath
                 )
 
     if ext in __extensions["Ruby"]:
@@ -145,7 +145,7 @@
 
     classBrowserModule = getClassBrowserModule(moduleType)
     dictionary = (
-        classBrowserModule.readmodule_ex(module, path, isTypeFile=isPyFile)
+        classBrowserModule.readmodule_ex(module, searchPath, isTypeFile=isPyFile)
         if classBrowserModule
         else {}
     )
@@ -244,6 +244,34 @@
     raise ImportError
 
 
+def determineSourceType(name, isPyFile=False):
+    """
+    Function to determine the type of a source file given its name.
+
+    @param name file name or module name
+    @type str
+    @param isPyFile flag indicating a Python file (defaults to False)
+    @type bool (optional)
+    @return source file type
+    @rtype int
+    """
+    ext = os.path.splitext(name)[1].lower()
+
+    if ext in __extensions["Ruby"]:
+        sourceType = RB_SOURCE
+    elif ext == ".ptl":
+        sourceType = PTL_SOURCE
+    elif (
+        name.lower().endswith(tuple(Preferences.getPython("Python3Extensions")))
+        or isPyFile
+    ):
+        sourceType = PY_SOURCE
+    else:
+        sourceType = UNKNOWN_SOURCE
+
+    return sourceType
+
+
 def getIcon(filename):
     """
     Function to get an icon name for the given file (only for class browsers provided
--- a/src/eric7/Utilities/ClassBrowsers/pyclbr.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Utilities/ClassBrowsers/pyclbr.py	Fri Feb 23 10:46:46 2024 +0100
@@ -20,6 +20,8 @@
 from PyQt6.QtCore import QRegularExpression
 
 from eric7 import Utilities
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities
 from eric7.Utilities import ClassBrowsers
 
 from . import ClbrBaseClasses
@@ -395,7 +397,7 @@
                 self.importedNames[name].append(lineno)
 
 
-def readmodule_ex(module, path=None, isTypeFile=False):
+def readmodule_ex(module, searchPath=None, isTypeFile=False):
     """
     Read a module file and return a dictionary of classes.
 
@@ -405,29 +407,38 @@
 
     @param module name of the module file
     @type str
-    @param path path the module should be searched in
+    @param searchPath path the module should be searched in
     @type list of str
     @param isTypeFile flag indicating a file of this type
     @type bool
     @return the resulting dictionary
     @rtype dict
     """
-    # search the path for the module
-    path = [] if path is None else path[:]
-    f = None
-    if f is None:
-        fullpath = path[:] + sys.path[:]
-        f, file, (suff, mode, type) = ClassBrowsers.find_module(
+    fsInterface = ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+
+    if searchPath and FileSystemUtilities.isRemoteFileName(searchPath[0]):
+        sourceType = ClassBrowsers.determineSourceType(module, isTypeFile)
+        file = fsInterface.join(searchPath[0], module)
+    else:
+        # search the path for the module
+        searchPath = [] if searchPath is None else searchPath[:]
+        fullpath = searchPath[:] + sys.path[:]
+        f, file, (suff, mode, sourceType) = ClassBrowsers.find_module(
             module, fullpath, isTypeFile
         )
-    if f:
-        f.close()
-    if type not in SUPPORTED_TYPES:
+        if f:
+            f.close()
+
+    if sourceType not in SUPPORTED_TYPES:
         # not Python source, can't do anything with this module
         return {}
 
     try:
-        src = Utilities.readEncodedFile(file)[0]
+        src = (
+            fsInterface.readEncodedFile(file)[0]
+            if FileSystemUtilities.isRemoteFileName(file)
+            else Utilities.readEncodedFile(file)[0]
+        )
     except (OSError, UnicodeError):
         # can't do anything with this module
         return {}
--- a/src/eric7/Utilities/ClassBrowsers/rbclbr.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Utilities/ClassBrowsers/rbclbr.py	Fri Feb 23 10:46:46 2024 +0100
@@ -17,6 +17,8 @@
 from PyQt6.QtCore import QRegularExpression
 
 from eric7 import Utilities
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities
 from eric7.Utilities import ClassBrowsers
 
 from . import ClbrBaseClasses
@@ -277,31 +279,41 @@
         self.setPrivate()
 
 
-def readmodule_ex(module, path=None, isTypeFile=False):  # noqa: U100
+def readmodule_ex(module, searchPath=None, isTypeFile=False):  # noqa: U100
     """
     Read a Ruby file and return a dictionary of classes, functions and modules.
 
     @param module name of the Ruby file
     @type str
-    @param path path the file should be searched in
+    @param searchPath path the file should be searched in
     @type list of str
     @param isTypeFile flag indicating a file of this type
     @type bool
     @return the resulting dictionary
     @rtype dict
     """
-    # search the path for the file
-    f = None
-    fullpath = [] if path is None else path[:]
-    f, file, (suff, mode, type) = ClassBrowsers.find_module(module, fullpath)
-    if f:
-        f.close()
-    if type not in SUPPORTED_TYPES:
+    fsInterface = ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+
+    if searchPath and FileSystemUtilities.isRemoteFileName(searchPath[0]):
+        sourceType = ClassBrowsers.determineSourceType(module)
+        file = fsInterface.join(searchPath[0], module)
+    else:
+        # search the path for the module
+        fullpath = [] if searchPath is None else searchPath[:]
+        f, file, (suff, mode, sourceType) = ClassBrowsers.find_module(module, fullpath)
+        if f:
+            f.close()
+
+    if sourceType not in SUPPORTED_TYPES:
         # not Ruby source, can't do anything with this module
         return {}
 
     try:
-        src = Utilities.readEncodedFile(file)[0]
+        src = (
+            fsInterface.readEncodedFile(file)[0]
+            if FileSystemUtilities.isRemoteFileName(file)
+            else Utilities.readEncodedFile(file)[0]
+        )
     except (OSError, UnicodeError):
         # can't do anything with this module
         return {}
--- a/src/eric7/Utilities/ModuleParser.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Utilities/ModuleParser.py	Fri Feb 23 10:46:46 2024 +0100
@@ -26,6 +26,8 @@
 from PyQt6.QtCore import QRegularExpression
 
 from eric7 import Utilities
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities
 
 __all__ = [
     "Attribute",
@@ -1677,42 +1679,50 @@
     @return reference to a Module object containing the parsed
         module information
     @rtype Module
+    @exception ImportError raised to indicate an unsupported source code type
     """
     global _modules
 
     _extensions = (
         [".py", ".pyw", ".pyi", ".ptl", ".rb"] if extensions is None else extensions[:]
     )
-    with contextlib.suppress(ValueError):
-        _extensions.remove(".py")
+    modname = module
+    isRemoteFileName = FileSystemUtilities.isRemoteFileName(module)
 
-    modname = module
+    if isRemoteFileName:
+        fsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
+        module, extension = fsInterface.splitext(fsInterface.basename(module))
+    else:
+        with contextlib.suppress(ValueError):
+            _extensions.remove(".py")
 
-    if os.path.exists(module):
-        path = [os.path.dirname(module)]
-        if module.lower().endswith(".py"):
-            module = module[:-3]
-        if (
-            os.path.exists(os.path.join(path[0], "__init__.py"))
-            or os.path.exists(os.path.join(path[0], "__init__.pyi"))
-            or os.path.exists(os.path.join(path[0], "__init__.rb"))
-            or inpackage
-        ):
-            if basename:
-                module = module.replace(basename, "")
-            if os.path.isabs(module):
-                modname = os.path.splitdrive(module)[1][len(os.sep) :]
+        if os.path.exists(module):
+            path = [os.path.dirname(module)]
+            if module.lower().endswith(".py"):
+                module = module[:-3]
+            if (
+                os.path.exists(os.path.join(path[0], "__init__.py"))
+                or os.path.exists(os.path.join(path[0], "__init__.pyi"))
+                or os.path.exists(os.path.join(path[0], "__init__.rb"))
+                or inpackage
+            ):
+                if basename:
+                    module = module.replace(basename, "")
+                if os.path.isabs(module):
+                    modname = os.path.splitdrive(module)[1][len(os.sep) :]
+                else:
+                    modname = module
+                modname = modname.replace(os.sep, ".")
+                inpackage = True
             else:
-                modname = module
-            modname = modname.replace(os.sep, ".")
-            inpackage = True
-        else:
-            modname = os.path.basename(module)
-        for ext in _extensions:
-            if modname.lower().endswith(ext):
-                modname = modname[: -len(ext)]
-                break
-        module = os.path.basename(module)
+                modname = os.path.basename(module)
+            for ext in _extensions:
+                if modname.lower().endswith(ext):
+                    modname = modname[: -len(ext)]
+                    break
+            module = os.path.basename(module)
 
     if caching and modname in _modules:
         # we've seen this module before...
@@ -1725,19 +1735,44 @@
             _modules[modname] = mod
         return mod
 
-    # search the path for the module
-    path = [] if path is None else path[:]
-    f = None
-    if inpackage:
-        try:
-            f, file, (suff, mode, moduleType) = find_module(module, path, _extensions)
-        except ImportError:
-            f = None
-    if f is None:
-        fullpath = path[:] + sys.path[:]
-        f, file, (suff, mode, moduleType) = find_module(module, fullpath, _extensions)
-    if f:
-        f.close()
+    if isRemoteFileName:
+        if not fsInterface.exists(modname):
+            raise ImportError
+
+        if extension == ".ptl":
+            moduleType = PTL_SOURCE
+        elif extension == ".rb":
+            moduleType = RB_SOURCE
+        elif extension in _extensions:  # noqa: Y106
+            moduleType = PY_SOURCE
+        else:
+            raise ImportError
+
+        file = modname
+
+        modname = FileSystemUtilities.plainFileName(modname)
+        if modname.startswith(("/", "\\")):
+            modname = modname[1:]
+        modname = os.path.splitext(modname)[0].replace("/", ".").replace("\\", ".")
+    else:
+        # search the path for the module
+        path = [] if path is None else path[:]
+        f = None
+        if inpackage:
+            try:
+                f, file, (suff, mode, moduleType) = find_module(
+                    module, path, _extensions
+                )
+            except ImportError:
+                f = None
+        if f is None:
+            fullpath = path[:] + sys.path[:]
+            f, file, (suff, mode, moduleType) = find_module(
+                module, fullpath, _extensions
+            )
+        if f:
+            f.close()
+
     if moduleType not in SUPPORTED_TYPES:
         # not supported source, can't do anything with this module
         _modules[modname] = Module(modname, None, None)
@@ -1745,7 +1780,14 @@
 
     mod = Module(modname, file, moduleType)
     with contextlib.suppress(UnicodeError, OSError):
-        src = Utilities.readEncodedFile(file)[0]
+        src = (
+            ericApp()
+            .getObject("EricServer")
+            .getServiceInterface("FileSystem")
+            .readEncodedFile(file)[0]
+            if isRemoteFileName
+            else Utilities.readEncodedFile(file)[0]
+        )
         mod.scan(src)
     if caching:
         _modules[modname] = mod
--- a/src/eric7/Utilities/__init__.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/Utilities/__init__.py	Fri Feb 23 10:46:46 2024 +0100
@@ -35,7 +35,7 @@
 
 from eric7 import Preferences
 from eric7.EricWidgets.EricApplication import ericApp
-from eric7.SystemUtilities import DesktopUtilities, OSUtilities
+from eric7.SystemUtilities import DesktopUtilities, FileSystemUtilities, OSUtilities
 from eric7.UI.Info import Program, Version
 
 
@@ -338,6 +338,32 @@
     return str(text, "utf-8", "ignore"), "utf-8-ignore"
 
 
+def decodeWithEncoding(text, encoding):
+    """
+    Function to decode some byte text into a string.
+
+    @param text byte text to decode
+    @type bytes
+    @param encoding encoding to be used to read the file
+    @type str
+    @return tuple of decoded text and encoding
+    @rtype tuple of (str, str)
+    """
+    if encoding:
+        with contextlib.suppress(UnicodeError, LookupError):
+            return str(text, encoding), "{0}-selected".format(encoding)
+
+        # Try default encoding
+        with contextlib.suppress(UnicodeError, LookupError):
+            codec = Preferences.getEditor("DefaultEncoding")
+            return str(text, codec), "{0}-default".format(codec)
+
+        # Assume UTF-8 loosing information
+        return str(text, "utf-8", "ignore"), "utf-8-ignore"
+    else:
+        return decode(text)
+
+
 def readEncodedFileWithEncoding(filename, encoding):
     """
     Function to read a file and decode its contents into proper text.
@@ -351,19 +377,7 @@
     """
     with open(filename, "rb") as f:
         text = f.read()
-    if encoding:
-        with contextlib.suppress(UnicodeError, LookupError):
-            return str(text, encoding), "{0}-selected".format(encoding)
-
-        # Try default encoding
-        with contextlib.suppress(UnicodeError, LookupError):
-            codec = Preferences.getEditor("DefaultEncoding")
-            return str(text, codec), "{0}-default".format(codec)
-
-        # Assume UTF-8 loosing information
-        return str(text, "utf-8", "ignore"), "utf-8-ignore"
-    else:
-        return decode(text)
+    return decodeWithEncoding(text, encoding)
 
 
 def writeEncodedFile(filename, text, origEncoding, forcedEncoding=""):
@@ -889,6 +903,16 @@
     basename = os.path.splitext(fn)[0]
     filename = "{0}.coverage".format(basename)
     if mustExist:
+        if FileSystemUtilities.isRemoteFileName(fn):
+            ericServer = ericApp().getObject("EricServer")
+            if ericServer.isServerConnected() and ericServer.getServiceInterface(
+                "FileSystem"
+            ).exists(filename):
+                return filename
+            else:
+                return ""
+
+        # It is a local file.
         if os.path.isfile(filename):
             return filename
         else:
@@ -929,12 +953,22 @@
     basename = os.path.splitext(fn)[0]
     filename = "{0}.profile".format(basename)
     if mustExist:
+        if FileSystemUtilities.isRemoteFileName(fn):
+            ericServer = ericApp().getObject("EricServer")
+            if ericServer.isServerConnected() and ericServer.getServiceInterface(
+                "FileSystem"
+            ).exists(filename):
+                return filename
+            else:
+                return ""
+
+        # It is a local file.
         if os.path.isfile(filename):
             return filename
         else:
             return ""
-    else:
-        return filename
+
+    return filename
 
 
 def parseOptionString(s):
--- a/src/eric7/ViewManager/ViewManager.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/ViewManager/ViewManager.py	Fri Feb 23 10:46:46 2024 +0100
@@ -39,6 +39,7 @@
 from eric7.QScintilla.SpellChecker import SpellChecker
 from eric7.QScintilla.SpellingDictionaryEditDialog import SpellingDictionaryEditDialog
 from eric7.QScintilla.ZoomDialog import ZoomDialog
+from eric7.RemoteServerInterface import EricServerFileDialog
 from eric7.SystemUtilities import FileSystemUtilities, OSUtilities
 
 
@@ -148,7 +149,7 @@
         self.__watcher = QFileSystemWatcher(self)
         self.__watcher.fileChanged.connect(self.__watchedFileChanged)
 
-    def setReferences(self, ui, dbs):
+    def setReferences(self, ui, dbs, remoteServerInterface):
         """
         Public method to set some references needed later on.
 
@@ -156,11 +157,16 @@
         @type UserInterface
         @param dbs reference to the debug server object
         @type DebugServer
+        @param remoteServerInterface reference to the 'eric-ide' server interface
+        @type EricServerInterface
         """
         from eric7.QScintilla.SearchReplaceWidget import SearchReplaceSlidingWidget
 
         self.ui = ui
         self.dbs = dbs
+        self.__remotefsInterface = remoteServerInterface.getServiceInterface(
+            "FileSystem"
+        )
 
         self.__searchReplaceWidget = SearchReplaceSlidingWidget(self, ui)
 
@@ -650,6 +656,30 @@
         self.openAct.triggered.connect(self.__openFiles)
         self.fileActions.append(self.openAct)
 
+        self.openRemoteAct = EricAction(
+            QCoreApplication.translate("ViewManager", "Open (Remote)"),
+            EricPixmapCache.getIcon("open-remote"),
+            QCoreApplication.translate("ViewManager", "Open (Remote)..."),
+            0,
+            0,
+            self,
+            "vm_file_open_remote",
+        )
+        self.openRemoteAct.setStatusTip(
+            QCoreApplication.translate("ViewManager", "Open a remote file")
+        )
+        self.openRemoteAct.setWhatsThis(
+            QCoreApplication.translate(
+                "ViewManager",
+                """<b>Open a remote file</b>"""
+                """<p>You will be asked for the name of a remote file to be opened"""
+                """ in an editor window.</p>""",
+            )
+        )
+        self.openRemoteAct.triggered.connect(self.__openRemoteFiles)
+        self.openRemoteAct.setEnabled(False)
+        self.fileActions.append(self.openRemoteAct)
+
         self.closeActGrp = createActionGroup(self)
 
         self.closeAct = EricAction(
@@ -745,13 +775,40 @@
             QCoreApplication.translate(
                 "ViewManager",
                 """<b>Save File as</b>"""
-                """<p>Save the contents of current editor window to a new file."""
+                """<p>Save the contents of the current editor window to a new file."""
                 """ The file can be entered in a file selection dialog.</p>""",
             )
         )
         self.saveAsAct.triggered.connect(self.saveAsCurrentEditor)
         self.fileActions.append(self.saveAsAct)
 
+        self.saveAsRemoteAct = EricAction(
+            QCoreApplication.translate("ViewManager", "Save as (Remote)"),
+            EricPixmapCache.getIcon("fileSaveAsRemote"),
+            QCoreApplication.translate("ViewManager", "Save as (Remote)..."),
+            0,
+            0,
+            self.saveActGrp,
+            "vm_file_save_as_remote",
+        )
+        self.saveAsRemoteAct.setStatusTip(
+            QCoreApplication.translate(
+                "ViewManager",
+                "Save the current file to a new one on an eric-ide server",
+            )
+        )
+        self.saveAsRemoteAct.setWhatsThis(
+            QCoreApplication.translate(
+                "ViewManager",
+                """<b>Save File as (Remote)</b>"""
+                """<p>Save the contents of the current editor window to a new file"""
+                """ on the connected eric-ide server. The file can be entered in a"""
+                """ file selection dialog.</p>""",
+            )
+        )
+        self.saveAsRemoteAct.triggered.connect(self.saveAsRemoteCurrentEditor)
+        self.fileActions.append(self.saveAsRemoteAct)
+
         self.saveCopyAct = EricAction(
             QCoreApplication.translate("ViewManager", "Save Copy"),
             EricPixmapCache.getIcon("fileSaveCopy"),
@@ -896,6 +953,7 @@
 
         menu.addAction(self.newAct)
         menu.addAction(self.openAct)
+        menu.addAction(self.openRemoteAct)
         self.menuRecentAct = menu.addMenu(self.recentMenu)
         menu.addMenu(self.bookmarkedMenu)
         menu.addSeparator()
@@ -906,6 +964,7 @@
         menu.addSeparator()
         menu.addAction(self.saveAct)
         menu.addAction(self.saveAsAct)
+        menu.addAction(self.saveAsRemoteAct)
         menu.addAction(self.saveCopyAct)
         menu.addAction(self.saveAllAct)
         self.exportersMenuAct = menu.addMenu(self.exportersMenu)
@@ -938,10 +997,12 @@
 
         tb.addAction(self.newAct)
         tb.addAction(self.openAct)
+        tb.addAction(self.openRemoteAct)
         tb.addAction(self.closeAct)
         tb.addSeparator()
         tb.addAction(self.saveAct)
         tb.addAction(self.saveAsAct)
+        tb.addAction(self.saveAsRemoteAct)
         tb.addAction(self.saveCopyAct)
         tb.addAction(self.saveAllAct)
 
@@ -5389,20 +5450,17 @@
     ## Methods and slots that deal with file and window handling
     ##################################################################
 
+    @pyqtSlot()
     def __openFiles(self):
         """
         Private slot to open some files.
         """
-        # set the cwd of the dialog based on the following search criteria:
-        #     1: Directory of currently active editor
-        #     2: Directory of currently active project
-        #     3: CWD
         from eric7.QScintilla import Lexers
 
         fileFilter = self._getOpenFileFilter()
         progs = EricFileDialog.getOpenFileNamesAndFilter(
             self.ui,
-            QCoreApplication.translate("ViewManager", "Open files"),
+            QCoreApplication.translate("ViewManager", "Open Files"),
             self._getOpenStartDir(),
             Lexers.getOpenFileFiltersList(True, True),
             fileFilter,
@@ -5410,6 +5468,33 @@
         for prog in progs:
             self.openFiles(prog)
 
+    @pyqtSlot()
+    def __openRemoteFiles(self):
+        """
+        Private slot to open some files.
+        """
+        from eric7.QScintilla import Lexers
+
+        if self.ui.isEricServerConnected():
+            fileFilter = self._getOpenFileFilter()
+            progs = EricServerFileDialog.getOpenFileNames(
+                self.ui,
+                QCoreApplication.translate("ViewManager", "Open Remote Files"),
+                self._getOpenStartDir(forRemote=True),
+                Lexers.getOpenFileFiltersList(True, True),
+                fileFilter,
+            )
+            for prog in progs:
+                self.openFiles(prog)
+        else:
+            EricMessageBox.critical(
+                self.ui,
+                self.tr("Open Remote Files"),
+                self.tr(
+                    "You must be connected to a remote eric-ide server. Aborting..."
+                ),
+            )
+
     def openFiles(self, prog):
         """
         Public slot to open some files.
@@ -5417,7 +5502,8 @@
         @param prog name of file to be opened
         @type str
         """
-        prog = os.path.abspath(prog)
+        if FileSystemUtilities.isPlainFileName(prog):
+            prog = os.path.abspath(prog)
         # Open up the new files.
         self.openSourceFile(prog)
 
@@ -5618,6 +5704,26 @@
             if FileSystemUtilities.isDeviceFileName(editor.getFileName()):
                 self.closeEditor(editor, ignoreDirty=True)
 
+    @pyqtSlot()
+    def closeRemoteEditors(self):
+        """
+        Public slot to close all editors related to a connected eric-ide server.
+        """
+        for editor in self.editors[:]:
+            if FileSystemUtilities.isRemoteFileName(editor.getFileName()):
+                self.closeEditor(editor, ignoreDirty=True)
+
+    @pyqtSlot(bool)
+    def remoteConnectionChanged(self, connected):
+        """
+        Public slot handling a change of the connection state to an eric-ide server.
+
+        @param connected flag indicating the connection state
+        @type bool
+        """
+        self.openRemoteAct.setEnabled(connected)
+        self.saveAsRemoteAct.setEnabled(self.saveActGrp.isEnabled() and connected)
+
     def exit(self):
         """
         Public method to handle the debugged program terminating.
@@ -6013,9 +6119,15 @@
         filenames = []
         for editor in self.editors:
             fn = editor.getFileName()
-            if fn is not None and fn not in filenames and os.path.exists(fn):
+            if fn is not None and fn not in filenames:
                 # only return names of existing files
-                filenames.append(fn)
+                exists = (
+                    self.__remotefsInterface.exists(fn)
+                    if FileSystemUtilities.isRemoteFileName(fn)
+                    else os.path.exists(fn)
+                )
+                if exists:
+                    filenames.append(fn)
 
         return filenames
 
@@ -6227,6 +6339,29 @@
         aw = self.activeWindow()
         self.saveAsEditorEd(aw)
 
+    @pyqtSlot(Editor)
+    def saveAsRemoteEditorEd(self, ed):
+        """
+        Public slot to save the contents of an editor to a new file on a
+        connected eric-ide server.
+
+        @param ed editor to be saved
+        @type Editor
+        """
+        if ed:
+            ok = ed.saveFileAs(remote=True)
+            if ok:
+                self.setEditorName(ed, ed.getFileName())
+
+    @pyqtSlot()
+    def saveAsRemoteCurrentEditor(self):
+        """
+        Public slot to save the contents of the current editor to a new file on a
+        connected eric-ide server.
+        """
+        aw = self.activeWindow()
+        self.saveAsRemoteEditorEd(aw)
+
     def saveCopyEditorEd(self, ed):
         """
         Public slot to save the contents of an editor to a new copy of
@@ -7610,6 +7745,7 @@
         """
         self.closeActGrp.setEnabled(True)
         self.saveActGrp.setEnabled(True)
+        self.saveAsRemoteAct.setEnabled(self.ui.isEricServerConnected())
         self.exportersMenuAct.setEnabled(True)
         self.printAct.setEnabled(True)
         if self.printPreviewAct:
@@ -7642,10 +7778,7 @@
         @type bool
         """
         if editor is not None:
-            self.saveAct.setEnabled(
-                editor.isModified()
-                and not FileSystemUtilities.isRemoteFileName(editor.getFileName())
-            )
+            self.saveAct.setEnabled(editor.isModified())
             self.revertAct.setEnabled(editor.isModified())
 
             self.undoAct.setEnabled(editor.isUndoAvailable())
@@ -8034,7 +8167,7 @@
     ## Below are protected utility methods
     ##################################################################
 
-    def _getOpenStartDir(self):
+    def _getOpenStartDir(self, forRemote=False):
         """
         Protected method to return the starting directory for a file open
         dialog.
@@ -8043,22 +8176,42 @@
         using the following search order, until a match is found:<br />
             1: Directory of currently active editor<br />
             2: Directory of currently active Project<br />
-            3: CWD
-
+            3: Directory defined as the workspace (only for local access)<br />
+            4: CWD
+
+        @param forRemote flag indicating to get the start directory for a remote
+            operation (defaults to False)
+        @type bool (optional)
         @return name of directory to start
         @rtype str
         """
         # if we have an active source, return its path
-        if self.activeWindow() is not None and self.activeWindow().getFileName():
-            return os.path.dirname(self.activeWindow().getFileName())
+        if self.activeWindow() is not None:
+            fn = self.activeWindow().getFileName()
+            if forRemote and FileSystemUtilities.isRemoteFileName(fn):
+                return (
+                    ericApp()
+                    .getObject("EricServer")
+                    .getServiceInterface("FileSystem")
+                    .dirname(fn)
+                )
+            if not forRemote and FileSystemUtilities.isPlainFileName(fn):
+                return os.path.dirname(fn)
 
         # check, if there is an active project and return its path
-        elif ericApp().getObject("Project").isOpen():
-            return ericApp().getObject("Project").ppath
-
-        else:
+        if ericApp().getObject("Project").isOpen():
+            ppath = ericApp().getObject("Project").ppath
+            if (forRemote and FileSystemUtilities.isRemoteFileName(ppath)) or (
+                not forRemote and FileSystemUtilities.isPlainFileName(ppath)
+            ):
+                return ppath
+
+        if not forRemote:
             return Preferences.getMultiProject("Workspace") or OSUtilities.getHomeDir()
 
+        # return empty string
+        return ""
+
     def _getOpenFileFilter(self):
         """
         Protected method to return the active filename filter for a file open
--- a/src/eric7/ViewManager/__init__.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/ViewManager/__init__.py	Fri Feb 23 10:46:46 2024 +0100
@@ -23,7 +23,7 @@
 ######################################################################
 
 
-def factory(ui, dbs, pluginManager):
+def factory(ui, dbs, remoteServerInterface, pluginManager):
     """
     Modul factory function to generate the right viewmanager type.
 
@@ -34,6 +34,8 @@
     @type UserInterface
     @param dbs reference to the debug server object
     @type DebugServer
+    @param remoteServerInterface reference to the 'eric-ide' server interface
+    @type EricServerInterface
     @param pluginManager reference to the plugin manager object
     @type PluginManager
     @return the instantiated viewmanager
@@ -48,5 +50,5 @@
         if vm is None:
             raise RuntimeError(f"Could not create a viemanager object.\nError: {err}")
         Preferences.setViewManager("tabview")
-    vm.setReferences(ui, dbs)
+    vm.setReferences(ui, dbs, remoteServerInterface)
     return vm
--- a/src/eric7/VirtualEnv/VirtualenvManager.py	Fri Feb 23 10:43:39 2024 +0100
+++ b/src/eric7/VirtualEnv/VirtualenvManager.py	Fri Feb 23 10:46:46 2024 +0100
@@ -26,6 +26,7 @@
 from .VirtualenvMeta import VirtualenvMetaData
 
 
+# TODO: introduce 'eric-ide Server' environment definitions
 class VirtualenvManager(QObject):
     """
     Class implementing an object to manage Python virtual environments.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/eric7_server.py	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+eric-ide Server.
+
+This is the main Python script of the eric-ide server. This is a server to perform
+remote development (e.g. code hosted on another computer or through a docker
+container).
+"""
+
+import argparse
+import socket
+import sys
+
+from eric7.RemoteServer.EricServer import EricServer
+from eric7.UI.Info import Version
+
+
+def createArgumentParser():
+    """
+    Function to create an argument parser.
+
+    @return created argument parser object
+    @rtype argparse.ArgumentParser
+    """
+    parser = argparse.ArgumentParser(
+        description=(
+            "Start the eric-ide server component. This will listen for connections"
+            " from the eric IDE in order to perform remote development."
+        ),
+        epilog="Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>.",
+    )
+
+    parser.add_argument(
+        "-p",
+        "--port",
+        type=int,
+        default=42024,
+        help="Listen on the given port for connections from an eric IDE.",
+    )
+    parser.add_argument(
+        "-6",
+        "--with-ipv6",
+        action="store_true",
+        help="Listen on IPv6 interfaces as well if the system supports the creation"
+        "of TCP sockets which can handle both IPv4 and IPv6. {0}".format(
+            "This system supports this feature."
+            if socket.has_dualstack_ipv6()
+            else "This system does not support this feature. Option will be ignored."
+        ),
+    )
+    parser.add_argument(
+        "-V",
+        "--version",
+        action="version",
+        version="%(prog)s {0}".format(Version),
+        help="Show version information and exit.",
+    )
+
+    return parser
+
+
+def main():
+    """
+    Main entry point into the application.
+    """
+    global supportedExtensions
+
+    parser = createArgumentParser()
+    args = parser.parse_args()
+
+    server = EricServer(port=args.port, useIPv6=args.with_ipv6)
+    ok = server.run()
+
+    sys.exit(0 if ok else 1)
+
+
+if __name__ == "__main__":
+    main()
+
+#
+# eflag: noqa = M801
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-dark/dialog-cancel.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="dialog-cancel.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.471276"
+     inkscape:cy="11.160662"
+     inkscape:window-width="2580"
+     inkscape:window-height="1255"
+     inkscape:window-x="692"
+     inkscape:window-y="13"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#eff0f1;
+      }
+      </style>
+  </defs>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25"
+     d="M 11,1 C 8.4655025,1 6.16085,1.9458675 4.3984375,3.5 4.2406387,3.63915 4.0784,3.780975 3.9296875,3.9296875 L 3.5,4.3984375 C 1.9458675,6.16085 1,8.4655025 1,11 c 0,5.52285 4.47715,10 10,10 2.534497,0 4.83915,-0.945867 6.601563,-2.5 l 0.46875,-0.429687 C 18.219025,17.9216 18.36085,17.759361 18.5,17.601563 20.054133,15.83915 21,13.534497 21,11 21,5.47715 16.52285,1 11,1 m 0,1.25 c 4.832487,0 8.75,3.9175125 8.75,8.75 0,2.192075 -0.816519,4.167835 -2.148437,5.703125 L 5.296875,4.3984375 C 6.8321637,3.0665187 8.807925,2.25 11,2.25 M 4.3984375,5.296875 16.703125,17.601563 C 15.167835,18.933481 13.192075,19.75 11,19.75 6.1675125,19.75 2.25,15.832487 2.25,11 2.25,8.807925 3.0665187,6.8321637 4.3984375,5.296875"
+     class="ColorScheme-Text"
+     id="path4" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-dark/dialog-ok.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="dialog-ok.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.449854"
+     inkscape:cy="11.26777"
+     inkscape:window-width="2580"
+     inkscape:window-height="1271"
+     inkscape:window-x="861"
+     inkscape:window-y="110"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#eff0f1;
+      }
+      </style>
+  </defs>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.291"
+     d="M 20.116087,3.0000134 7.0676375,17.088646 1.8839,11.491668 1,12.446045 l 5.1837375,5.596951 -0.0025,0.0026 0.8839125,0.954364 0.0025,-0.0026 0.0025,0.0026 0.8838875,-0.954364 -0.0025,-0.0026 L 21,3.9543498 20.1161,3 Z"
+     class="ColorScheme-Text"
+     id="path4" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-dark/dirNew.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="dirNew.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.492697"
+     inkscape:cy="11.278481"
+     inkscape:window-width="2580"
+     inkscape:window-height="1326"
+     inkscape:window-x="0"
+     inkscape:window-y="55"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#eff0f1;
+      }
+      </style>
+  </defs>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25"
+     d="M 1,1 V 2.2499998 20.999996 H 2.2499998 13.499998 v -1.25 H 2.2499998 V 9.7499984 h 3.7499993 v -0.012208 l 0.00977,0.012208 2.4999995,-2.4999996 H 19.749998 v 6.2499992 h 1.25 V 3.4999995 H 12.259764 L 9.7597646,1 9.7500021,1.0122125 V 1 H 2.2500035 1.0000037 M 17.250001,14.749997 v 2.5 h -2.5 v 1.25 h 2.5 v 2.499999 H 18.5 V 18.499997 H 21 v -1.25 h -2.5 v -2.5 h -1.249999"
+     class="ColorScheme-Text"
+     id="path4" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-dark/fileSaveAsRemote.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   version="1.1"
+   viewBox="0 0 22 22"
+   id="svg8"
+   sodipodi:docname="fileSaveAsRemote.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview10"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="12.028238"
+     inkscape:cy="11.546251"
+     inkscape:window-width="2580"
+     inkscape:window-height="1321"
+     inkscape:window-x="860"
+     inkscape:window-y="59"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg8" />
+  <defs
+     id="defs4">
+    <style
+       type="text/css"
+       id="style2">.ColorScheme-Text {
+        color:#eff0f1;
+      }</style>
+  </defs>
+  <path
+     class="ColorScheme-Text"
+     d="M 2,1.0001509 V 15.994085 h 9.000109 v -0.93707 H 6.0000082 V 9.4348532 H 11.000109 V 8.4977845 H 5.0000061 V 15.057015 H 3.000002 V 1.9388863 H 6.0000082 V 6.6240635 H 14.000114 V 1.9388863 h 0.292971 l 2.707054,2.5365439 V 7.561049 h 1.000002 V 4.0947694 h -0.0077 l 0.0077,-0.00915 L 14.707092,1 14.699256,1.00915 V 1 H 14.000034 Z M 7.0001004,1.9389695 H 10.900567 V 5.6870781 H 7.0001004 Z M 18.000033,8.4982011 17.003901,9.4297698 h -0.0038 L 12,14.113115 l 0.0077,0.0092 -0.0039,0.940653 H 11.9999 V 16 h 2.000071 l 0.0059,-0.0092 h 0.01364 l -0.0059,-0.0092 0.98632,-0.924234 4.00001,-3.744443 -0.705086,-0.664339 -4.990291,4.672345 -0.593755,-0.554534 4.99227,-4.674178 0.591802,0.556359 0.705087,0.658847 L 20,10.37034 Z"
+     color="#eff0f1"
+     fill="currentColor"
+     id="path6"
+     style="stroke-width:0.866025" />
+  <path
+     style="color:#eff0f1;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099"
+     d="m 8.4999983,17 v 1.333333 H 1 v 1.333334 H 8.4999983 V 21 h 4.9999987 v -1.333333 h 7.499999 V 18.333333 H 13.499997 V 17 H 8.4999983 m 2.4999997,0.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.2499999,-0.594666 -1.2499999,-1.333333 0,-0.738667 0.5574999,-1.333333 1.2499999,-1.333333"
+     class="ColorScheme-Text"
+     id="path4"
+     sodipodi:nodetypes="ccccccccccccccsssc" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-dark/open-remote.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="open-remote.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.621227"
+     inkscape:cy="11.332035"
+     inkscape:window-width="2580"
+     inkscape:window-height="1337"
+     inkscape:window-x="861"
+     inkscape:window-y="44"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#eff0f1;
+      }
+      </style>
+  </defs>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25"
+     d="M 1,1 V 2.25 16 H 2.2499998 V 9.75 H 5.9999991 V 9.74023 L 6.0097653,9.75 8.5097648,7.25 H 19.749998 V 16 h 1.25 V 3.5 H 12.259764 L 9.7597646,1 9.7500046,1.00976 V 1 H 2.2500035 1.0000037 M 8.5000023,17.25 V 18.5 H 1.0000037 v 1.25 H 8.5000023 V 21 H 13.500001 V 19.75 H 21 V 18.5 H 13.500001 V 17.25 H 8.5000023 m 2.4999997,0.625 c 0.6925,0 1.25,0.5575 1.25,1.25 0,0.6925 -0.5575,1.25 -1.25,1.25 -0.6925,0 -1.2499999,-0.5575 -1.2499999,-1.25 0,-0.6925 0.5574999,-1.25 1.2499999,-1.25"
+     class="ColorScheme-Text"
+     id="path4" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-dark/preferences-eric-server.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="22"
+   width="22"
+   version="1.1"
+   id="svg1718"
+   sodipodi:docname="preferences-eric-server.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1722" />
+  <sodipodi:namedview
+     id="namedview1720"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="32.09375"
+     inkscape:cx="16.623174"
+     inkscape:cy="16.327167"
+     inkscape:window-width="2580"
+     inkscape:window-height="1381"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg1718" />
+  <linearGradient
+     id="b"
+     gradientTransform="matrix(0.7,0,0,0.7,-0.7,-1.1)"
+     gradientUnits="userSpaceOnUse"
+     x2="0"
+     y1="44"
+     y2="4">
+    <stop
+       offset="0"
+       stop-color="#1d1e1e"
+       id="stop1680" />
+    <stop
+       offset="1"
+       stop-color="#44484c"
+       id="stop1682" />
+  </linearGradient>
+  <linearGradient
+     id="c"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-350.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="a"
+     gradientUnits="userSpaceOnUse"
+     x2="0"
+     y1="507.79999"
+     y2="506.79999">
+    <stop
+       offset="0"
+       stop-color="#3da103"
+       id="stop1686" />
+    <stop
+       offset="1"
+       stop-color="#7ddf07"
+       id="stop1688" />
+  </linearGradient>
+  <linearGradient
+     id="d"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-339.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="e"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-328.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="f"
+     gradientUnits="userSpaceOnUse"
+     x1="5"
+     x2="18"
+     y1="12"
+     y2="25">
+    <stop
+       offset="0"
+       stop-color="#292c2f"
+       id="stop1693"
+       style="stop-color:#c0c0c0;stop-opacity:1;" />
+    <stop
+       offset="1"
+       stop-opacity="0"
+       id="stop1695" />
+  </linearGradient>
+  <g
+     id="g2294"
+     transform="matrix(0.72413793,0,0,0.71428571,-1.1724138,-0.42857143)">
+    <path
+       d="M 3,7 H 29 V 24 H 3 Z"
+       fill="#111213"
+       id="path1698" />
+    <path
+       d="M 3,2 V 30 H 29 V 2 Z m 1,6 h 24 v 4 H 4 Z m 0,11 h 24 v 4 H 4 Z"
+       fill="url(#b)"
+       id="path1700"
+       style="fill:#ececec;stroke-width:0.7" />
+    <path
+       d="M 5,4 H 7 V 5 H 5 Z"
+       fill="url(#c)"
+       id="path1702"
+       style="fill:url(#c);stroke-width:0.7" />
+    <path
+       d="m 5,15 h 2 v 1 H 5 Z"
+       fill="url(#d)"
+       id="path1704"
+       style="fill:url(#d);stroke-width:0.7" />
+    <path
+       d="m 5,26 h 2 v 1 H 5 Z"
+       fill="url(#e)"
+       id="path1706"
+       style="fill:url(#e);stroke-width:0.7" />
+    <path
+       d="m 3,29 h 26 v 1 H 3 Z"
+       opacity="0.2"
+       id="path1710" />
+    <path
+       d="m 4,12 7,7 h 17 v 4 H 15 l 7,7 h 7 V 13 l -1,-1 z"
+       fill="url(#f)"
+       fill-rule="evenodd"
+       opacity="0.4"
+       id="path1712"
+       style="fill:url(#f)" />
+    <rect
+       fill="#eff0f1"
+       height="16"
+       rx="2"
+       width="16"
+       x="16"
+       y="14"
+       id="rect1714" />
+    <path
+       d="m 19,16 v 2 h -1 v 2 h 1 v 4 h -1 v 2 h 1 v 2 H 30 V 16 Z m 1,1 h 1 v 10 h -1 z m 2,0 h 7 v 10 h -7 z"
+       fill="#232629"
+       id="path1716" />
+  </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-dark/projectOpen-remote.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   version="1.1"
+   viewBox="0 0 22 22"
+   id="svg10"
+   sodipodi:docname="projectOpen-remote.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview12"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="12.028238"
+     inkscape:cy="11.546251"
+     inkscape:window-width="2580"
+     inkscape:window-height="1285"
+     inkscape:window-x="861"
+     inkscape:window-y="96"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg10" />
+  <defs
+     id="defs4">
+    <style
+       type="text/css"
+       id="style2">.ColorScheme-Text {
+        color:#eff0f1;
+      }</style>
+  </defs>
+  <g
+     id="g4755"
+     transform="matrix(0.75,0,0,0.75,2.75,0.25)">
+    <path
+       d="M 3.5,1 V 3.5 H 1 v 3.75 h 2.5 v 7.5 H 1 V 18.5 H 3.5 V 21 H 11 V 19.75 H 4.75 V 6 h 15 v 5 H 21 V 1 Z m 1.25,1.25 h 15 v 2.5 h -15 z"
+       color="#eff0f1"
+       fill="currentColor"
+       id="path6" />
+    <path
+       class="ColorScheme-Text"
+       d="m 12,12 v 9 h 9 V 13.5 H 17.2559 L 15.7559,12 15.75,12.0059 V 12 h -3 z m 0.75,0.75 h 2.6938 l 0.75146,0.75 h -0.44531 v 0.0059 l -0.0058,-0.0059 -1.5,1.5 h -1.4941 z m 0,3 h 7.5 v 4.5 h -7.5 z"
+       color="#eff0f1"
+       fill="currentColor"
+       id="path8" />
+  </g>
+  <path
+     style="color:#eff0f1;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099"
+     d="m 8.4999998,17 v 1.333333 H 1 v 1.333334 H 8.4999998 V 21 H 13.499999 V 19.666667 H 21 V 18.333333 H 13.499999 V 17 H 8.4999998 M 11,17.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.2500002,-0.594666 -1.2500002,-1.333333 0,-0.738667 0.5575002,-1.333333 1.2500002,-1.333333"
+     class="ColorScheme-Text"
+     id="path4"
+     sodipodi:nodetypes="ccccccccccccccsssc" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-dark/projectSaveAs-remote.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   version="1.1"
+   viewBox="0 0 22 22"
+   id="svg10"
+   sodipodi:docname="projectSaveAs-remote.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview12"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="12.135346"
+     inkscape:cy="11.567673"
+     inkscape:window-width="2580"
+     inkscape:window-height="1281"
+     inkscape:window-x="861"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg10" />
+  <defs
+     id="defs4">
+    <style
+       type="text/css"
+       id="style2">.ColorScheme-Text {
+        color:#eff0f1;
+      }</style>
+  </defs>
+  <g
+     id="g2432"
+     transform="matrix(0.74999663,0,0,0.74999663,2.7500034,0.25000337)">
+    <g
+       id="g2427">
+      <path
+         d="M 3.5,1 V 3.5 H 1 v 3.75 h 2.5 v 7.5 H 1 V 18.5 H 3.5 V 21 H 11 V 19.75 H 4.75 V 6 h 15 v 5 H 21 V 1 Z m 1.25,1.25 h 15 v 2.5 h -15 z"
+         color="#eff0f1"
+         fill="currentColor"
+         id="path6" />
+    </g>
+    <path
+       class="ColorScheme-Text"
+       d="m 12,12.5 v 8.4969 h 4.5 V 20.46594 H 14 v -3.186 h 2.5 v -0.531 h -3 v 3.717 h -1 v -7.4339 H 14 v 2.655 h 4 v -2.655 h 0.14648 l 1.3535,1.4374 v 1.7486 h 0.5 v -1.9643 h -0.0039 l 0.0039,-0.0051 -1.6465,-1.7486 -0.0039,0.0051 v -0.0051 h -0.34961 z m 2.5,0.53203 h 1.9502 v 2.124 H 14.5 Z m 5.5,3.717 -0.49805,0.52788 h -0.0019 l -2.5,2.6539 0.0039,0.0051 -0.0019,0.53307 h -0.0019 v 0.531 h 1 l 0.0029,-0.0051 h 0.0068 l -0.0029,-0.0051 0.49316,-0.52374 2,-2.1219 -0.35254,-0.37647 -2.4951,2.6477 -0.29688,-0.31424 2.4961,-2.6488 0.2959,0.31528 0.35254,0.37336 0.49996,-0.531 z"
+       color="#eff0f1"
+       fill="currentColor"
+       id="path8" />
+  </g>
+  <path
+     style="color:#eff0f1;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099"
+     d="m 8.4999998,17 v 1.333333 H 1 v 1.333334 H 8.4999998 V 21 H 13.499999 V 19.666667 H 21 V 18.333333 H 13.499999 V 17 H 8.4999998 M 11,17.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.2500002,-0.594666 -1.2500002,-1.333333 0,-0.738667 0.5575002,-1.333333 1.2500002,-1.333333"
+     class="ColorScheme-Text"
+     id="path4"
+     sodipodi:nodetypes="ccccccccccccccsssc" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-light/dialog-cancel.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="dialog-cancel.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.53554"
+     inkscape:cy="11.332035"
+     inkscape:window-width="2580"
+     inkscape:window-height="1327"
+     inkscape:window-x="861"
+     inkscape:window-y="54"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#232629;
+      }
+      </style>
+  </defs>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25"
+     d="M 11,1 C 8.4655025,1 6.16085,1.9458675 4.3984375,3.5 4.2406388,3.63915 4.0784,3.780975 3.9296875,3.9296875 L 3.5,4.3984375 C 1.9458675,6.16085 1,8.4655025 1,11 c 0,5.52285 4.47715,10 10,10 2.534497,0 4.83915,-0.945867 6.601562,-2.5 l 0.46875,-0.429688 C 18.219025,17.9216 18.36085,17.759361 18.5,17.601562 20.054133,15.83915 21,13.534497 21,11 21,5.47715 16.52285,1 11,1 m 0,1.25 c 4.832487,0 8.75,3.9175125 8.75,8.75 0,2.192075 -0.816519,4.167835 -2.148438,5.703125 L 5.296875,4.3984375 C 6.8321638,3.0665187 8.807925,2.25 11,2.25 M 4.3984375,5.296875 16.703125,17.601562 C 15.167835,18.933481 13.192075,19.75 11,19.75 6.1675125,19.75 2.25,15.832487 2.25,11 2.25,8.807925 3.0665187,6.8321638 4.3984375,5.296875"
+     class="ColorScheme-Text"
+     id="path4" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-light/dialog-ok.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="dialog-ok.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.514119"
+     inkscape:cy="11.26777"
+     inkscape:window-width="2580"
+     inkscape:window-height="1335"
+     inkscape:window-x="861"
+     inkscape:window-y="46"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#232629;
+      }
+      </style>
+  </defs>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099"
+     d="M 20.116087,3.0000133 7.0676375,17.088623 1.8839,11.491654 1,12.446029 l 5.1837375,5.596943 -0.0025,0.0027 L 7.06515,19 7.06765,18.997333 7.07015,19 7.9540375,18.045638 7.9515375,18.042972 21,3.9543483 20.1161,3 Z"
+     class="ColorScheme-Text"
+     id="path4" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-light/dirNew.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="dirNew.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.514119"
+     inkscape:cy="11.246349"
+     inkscape:window-width="2580"
+     inkscape:window-height="1351"
+     inkscape:window-x="0"
+     inkscape:window-y="30"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#232629;
+      }
+      </style>
+  </defs>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25"
+     d="M 1,1 V 2.2499998 20.999996 H 2.2499998 13.499998 v -1.25 H 2.2499998 V 9.7499984 h 3.7499993 v -0.012208 l 0.00977,0.012208 2.4999995,-2.4999996 H 19.749998 v 6.2499992 h 1.25 V 3.4999995 H 12.259764 L 9.7597646,1 9.7500021,1.0122125 V 1 H 2.2500035 1.0000037 M 17.250001,14.749997 v 2.5 h -2.5 v 1.25 h 2.5 v 2.499999 H 18.5 V 18.499997 H 21 v -1.25 h -2.5 v -2.5 h -1.249999"
+     class="ColorScheme-Text"
+     id="path4" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-light/fileSaveAsRemote.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   version="1.1"
+   viewBox="0 0 22 22"
+   id="svg8"
+   sodipodi:docname="fileSaveAsRemote.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview10"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.53554"
+     inkscape:cy="11.246349"
+     inkscape:window-width="2580"
+     inkscape:window-height="1381"
+     inkscape:window-x="861"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg8" />
+  <defs
+     id="defs4">
+    <style
+       type="text/css"
+       id="style2">.ColorScheme-Text {
+        color:#eff0f1;
+      }</style>
+  </defs>
+  <path
+     class="ColorScheme-Text"
+     d="M 2,1.0001508 V 15.994083 h 9.000144 V 15.057015 H 6.000024 V 9.4348525 h 5.00012 V 8.4977834 H 5.000018 V 15.057015 H 3.000006 V 1.9388862 h 3.000018 v 4.6851769 h 8.000138 V 1.9388862 h 0.292973 L 17.000198,4.47543 v 3.0856185 h 1.000006 V 4.0947693 h -0.0078 l 0.0078,-0.00915 L 14.707141,1 14.699341,1.00915 V 1 h -0.69923 z m 5.00012,0.9388187 h 3.900482 V 5.6870777 H 7.00012 Z M 18.000096,8.4982001 17.00396,9.4297694 h -0.0039 l -5.00012,4.6833436 0.0078,0.0092 -0.0039,0.940652 h -0.0039 V 16 h 2.000012 l 0.0059,-0.0092 h 0.01364 l -0.0059,-0.0092 0.986326,-0.924235 4.000024,-3.744443 -0.705089,-0.664339 -4.99031,4.672344 -0.593758,-0.554533 4.99229,-4.674177 0.591805,0.556358 0.705089,0.658848 L 20,10.370438 Z"
+     color="#eff0f1"
+     fill="#232629"
+     id="path6"
+     style="stroke-width:0.866025" />
+  <path
+     style="color:#232629;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099"
+     d="m 8.4999983,17 v 1.333333 H 1 v 1.333334 H 8.4999983 V 21 h 4.9999987 v -1.333333 h 7.499999 V 18.333333 H 13.499997 V 17 H 8.4999983 m 2.4999997,0.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.2499999,-0.594666 -1.2499999,-1.333333 0,-0.738667 0.5574999,-1.333333 1.2499999,-1.333333"
+     class="ColorScheme-Text"
+     id="path4"
+     sodipodi:nodetypes="ccccccccccccccsssc" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-light/open-remote.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   viewBox="0 0 22 22"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="open-remote.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.514119"
+     inkscape:cy="11.26777"
+     inkscape:window-width="2580"
+     inkscape:window-height="1333"
+     inkscape:window-x="861"
+     inkscape:window-y="48"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <defs
+     id="defs3051">
+    <style
+       type="text/css"
+       id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#232629;
+      }
+      </style>
+  </defs>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25"
+     d="M 1,1 V 2.25 16 H 2.2499998 V 9.75 H 5.9999991 V 9.74023 L 6.0097653,9.75 8.5097648,7.25 H 19.749998 V 16 h 1.25 V 3.5 H 12.259764 L 9.7597646,1 9.7500046,1.00976 V 1 H 2.2500035 1.0000037 M 8.5000023,17.25 V 18.5 H 1.0000037 v 1.25 H 8.5000023 V 21 H 13.500001 V 19.75 H 21 V 18.5 H 13.500001 V 17.25 H 8.5000023 m 2.4999997,0.625 c 0.6925,0 1.25,0.5575 1.25,1.25 0,0.6925 -0.5575,1.25 -1.25,1.25 -0.6925,0 -1.2499999,-0.5575 -1.2499999,-1.25 0,-0.6925 0.5574999,-1.25 1.2499999,-1.25"
+     class="ColorScheme-Text"
+     id="path4" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-light/preferences-eric-server.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="22"
+   width="22"
+   version="1.1"
+   id="svg3827"
+   sodipodi:docname="preferences-eric-server.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs3831" />
+  <sodipodi:namedview
+     id="namedview3829"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="32.09375"
+     inkscape:cx="16.747809"
+     inkscape:cy="16.389484"
+     inkscape:window-width="2580"
+     inkscape:window-height="1321"
+     inkscape:window-x="0"
+     inkscape:window-y="60"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg3827" />
+  <linearGradient
+     id="b"
+     gradientTransform="matrix(0.7,0,0,0.7,-0.7,-1.1)"
+     gradientUnits="userSpaceOnUse"
+     x2="0"
+     y1="44"
+     y2="4">
+    <stop
+       offset="0"
+       stop-color="#1d1e1e"
+       id="stop3789" />
+    <stop
+       offset="1"
+       stop-color="#44484c"
+       id="stop3791" />
+  </linearGradient>
+  <linearGradient
+     id="c"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-350.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="a"
+     gradientUnits="userSpaceOnUse"
+     x2="0"
+     y1="507.79999"
+     y2="506.79999">
+    <stop
+       offset="0"
+       stop-color="#3da103"
+       id="stop3795" />
+    <stop
+       offset="1"
+       stop-color="#7ddf07"
+       id="stop3797" />
+  </linearGradient>
+  <linearGradient
+     id="d"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-339.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="e"
+     gradientTransform="matrix(0.7,0,0,0.7,-270.499,-328.76)"
+     x2="0"
+     xlink:href="#a"
+     y1="507.79999"
+     y2="506.79999" />
+  <linearGradient
+     id="f"
+     gradientUnits="userSpaceOnUse"
+     x1="5"
+     x2="18"
+     y1="12"
+     y2="25">
+    <stop
+       offset="0"
+       stop-color="#292c2f"
+       id="stop3802" />
+    <stop
+       offset="1"
+       stop-opacity="0"
+       id="stop3804" />
+  </linearGradient>
+  <g
+     id="g5049"
+     transform="matrix(0.72413793,0,0,0.71428571,-1.1724138,-0.42857143)">
+    <path
+       d="M 3,7 H 29 V 24 H 3 Z"
+       fill="#111213"
+       id="path3807" />
+    <g
+       stroke-width="0.7"
+       id="g3817">
+      <path
+         d="M 3,2 V 30 H 29 V 2 Z m 1,6 h 24 v 4 H 4 Z m 0,11 h 24 v 4 H 4 Z"
+         fill="url(#b)"
+         id="path3809"
+         style="fill:url(#b)" />
+      <path
+         d="M 5,4 H 7 V 5 H 5 Z"
+         fill="url(#c)"
+         id="path3811"
+         style="fill:url(#c)" />
+      <path
+         d="m 5,15 h 2 v 1 H 5 Z"
+         fill="url(#d)"
+         id="path3813"
+         style="fill:url(#d)" />
+      <path
+         d="m 5,26 h 2 v 1 H 5 Z"
+         fill="url(#e)"
+         id="path3815"
+         style="fill:url(#e)" />
+    </g>
+    <path
+       d="m 3,29 h 26 v 1 H 3 Z"
+       opacity="0.2"
+       id="path3819" />
+    <path
+       d="m 4,12 7,7 h 17 v 4 H 15 l 7,7 h 7 V 13 l -1,-1 z"
+       fill="url(#f)"
+       fill-rule="evenodd"
+       opacity="0.4"
+       id="path3821"
+       style="fill:url(#f)" />
+    <rect
+       fill="#eff0f1"
+       height="16"
+       rx="2"
+       width="16"
+       x="16"
+       y="14"
+       id="rect3823" />
+    <path
+       d="m 19,16 v 2 h -1 v 2 h 1 v 4 h -1 v 2 h 1 v 2 H 30 V 16 Z m 1,1 h 1 v 10 h -1 z m 2,0 h 7 v 10 h -7 z"
+       fill="#232629"
+       id="path3825" />
+  </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-light/projectOpen-remote.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   version="1.1"
+   viewBox="0 0 22 22"
+   id="svg1062"
+   sodipodi:docname="projectOpen-remote.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1064"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.289192"
+     inkscape:cy="11.803311"
+     inkscape:window-width="1857"
+     inkscape:window-height="1337"
+     inkscape:window-x="62"
+     inkscape:window-y="17"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg1062" />
+  <defs
+     id="defs1056">
+    <style
+       type="text/css"
+       id="style1054">.ColorScheme-Text {
+        color:#eff0f1;
+      }</style>
+  </defs>
+  <g
+     id="g9300"
+     transform="matrix(0.75,0,0,0.75,2.75,0.25)">
+    <path
+       d="M 3.5,1 V 3.5 H 1 v 3.75 h 2.5 v 7.5 H 1 V 18.5 H 3.5 V 21 H 11 V 19.75 H 4.75 V 6 h 15 v 5 H 21 V 1 Z m 1.25,1.25 h 15 v 2.5 h -15 z"
+       color="#eff0f1"
+       fill="#232629"
+       id="path1058" />
+    <path
+       class="ColorScheme-Text"
+       d="m 12,12 v 9 h 9 V 13.5 H 17.2559 L 15.7559,12 15.75,12.0059 V 12 h -3 z m 0.75,0.75 h 2.6938 l 0.75146,0.75 h -0.44531 v 0.0059 l -0.0058,-0.0059 -1.5,1.5 h -1.4941 z m 0,3 h 7.5 v 4.5 h -7.5 z"
+       color="#eff0f1"
+       fill="#232629"
+       id="path1060" />
+  </g>
+  <path
+     style="color:#232629;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099"
+     d="m 8.7679983,17 v 1.333333 H 1.268 v 1.333334 H 8.7679983 V 21 h 4.9999987 v -1.333333 h 7.499999 V 18.333333 H 13.767997 V 17 H 8.7679983 m 2.4999997,0.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.25,-0.594666 -1.25,-1.333333 0,-0.738667 0.5575,-1.333333 1.25,-1.333333"
+     class="ColorScheme-Text"
+     id="path4"
+     sodipodi:nodetypes="ccccccccccccccsssc" />
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/icons/breeze-light/projectSaveAs-remote.svg	Fri Feb 23 10:46:46 2024 +0100
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   version="1.1"
+   viewBox="0 0 22 22"
+   id="svg10"
+   sodipodi:docname="projectSaveAs-remote.svg"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview12"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="46.681818"
+     inkscape:cx="11.407011"
+     inkscape:cy="11.214216"
+     inkscape:window-width="2580"
+     inkscape:window-height="1274"
+     inkscape:window-x="861"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg10" />
+  <defs
+     id="defs4">
+    <style
+       type="text/css"
+       id="style2">.ColorScheme-Text {
+        color:#eff0f1;
+      }</style>
+  </defs>
+  <g
+     id="g2428"
+     transform="matrix(0.74999663,0,0,0.74999663,2.7500034,0.25000337)">
+    <path
+       d="M 3.5,1 V 3.5 H 1 v 3.75 h 2.5 v 7.5 H 1 V 18.5 H 3.5 V 21 H 11 V 19.75 H 4.75 V 6 h 15 v 5 H 21 V 1 Z m 1.25,1.25 h 15 v 2.5 h -15 z"
+       color="#eff0f1"
+       fill="#232629"
+       id="path6" />
+    <path
+       class="ColorScheme-Text"
+       d="m 12,12.5 v 8.4969 h 4.5 V 20.46594 H 14 v -3.186 h 2.5 v -0.531 h -3 v 3.717 h -1 v -7.4339 H 14 v 2.655 h 4 v -2.655 h 0.14648 l 1.3535,1.4374 v 1.7486 h 0.5 v -1.9643 h -0.0039 l 0.0039,-0.0051 -1.6465,-1.7486 -0.0039,0.0051 v -0.0051 h -0.34961 z m 2.5,0.53203 h 1.9502 v 2.124 H 14.5 Z m 5.5,3.717 -0.49805,0.52788 h -0.0019 l -2.5,2.6539 0.0039,0.0051 -0.0019,0.53307 h -0.0019 v 0.531 h 1 l 0.0029,-0.0051 h 0.0068 l -0.0029,-0.0051 0.49316,-0.52374 2,-2.1219 -0.35254,-0.37647 -2.4951,2.6477 -0.29688,-0.31424 2.4961,-2.6488 0.2959,0.31528 0.35254,0.37336 0.49996,-0.531 z"
+       color="#eff0f1"
+       fill="#232629"
+       id="path8" />
+  </g>
+  <path
+     style="color:#232629;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099"
+     d="m 8.4999983,17 v 1.333333 H 1 v 1.333334 H 8.4999983 V 21 h 4.9999987 v -1.333333 h 7.499999 V 18.333333 H 13.499997 V 17 H 8.4999983 m 2.4999997,0.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.25,-0.594666 -1.25,-1.333333 0,-0.738667 0.5575,-1.333333 1.25,-1.333333"
+     class="ColorScheme-Text"
+     id="path4"
+     sodipodi:nodetypes="ccccccccccccccsssc" />
+</svg>

eric ide

mercurial