Implemented the JSON based session files. jsonfiles

Wed, 27 Jan 2021 15:09:20 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 27 Jan 2021 15:09:20 +0100
branch
jsonfiles
changeset 8009
29818ac4853c
parent 8006
c4110b8b5931
child 8010
7ce2b8bb0d9b

Implemented the JSON based session files.

eric6.e4p file | annotate | diff | comparison | revisions
eric6/Debugger/BreakPointModel.py file | annotate | diff | comparison | revisions
eric6/Debugger/DebugUI.py file | annotate | diff | comparison | revisions
eric6/Debugger/WatchPointModel.py file | annotate | diff | comparison | revisions
eric6/E5XML/SessionWriter.py file | annotate | diff | comparison | revisions
eric6/Project/Project.py file | annotate | diff | comparison | revisions
eric6/QScintilla/Lexers/__init__.py file | annotate | diff | comparison | revisions
eric6/Sessions/SessionFile.py file | annotate | diff | comparison | revisions
eric6/Sessions/__init__.py file | annotate | diff | comparison | revisions
eric6/UI/UserInterface.py file | annotate | diff | comparison | revisions
--- a/eric6.e4p	Mon Jan 25 20:07:51 2021 +0100
+++ b/eric6.e4p	Wed Jan 27 15:09:20 2021 +0100
@@ -912,6 +912,8 @@
     <Source>eric6/QScintilla/TypingCompleters/__init__.py</Source>
     <Source>eric6/QScintilla/ZoomDialog.py</Source>
     <Source>eric6/QScintilla/__init__.py</Source>
+    <Source>eric6/Sessions/SessionFile.py</Source>
+    <Source>eric6/Sessions/__init__.py</Source>
     <Source>eric6/Snapshot/SnapWidget.py</Source>
     <Source>eric6/Snapshot/SnapshotDefaultGrabber.py</Source>
     <Source>eric6/Snapshot/SnapshotFreehandGrabber.py</Source>
--- a/eric6/Debugger/BreakPointModel.py	Mon Jan 25 20:07:51 2021 +0100
+++ b/eric6/Debugger/BreakPointModel.py	Wed Jan 27 15:09:20 2021 +0100
@@ -7,6 +7,8 @@
 Module implementing the Breakpoint model.
 """
 
+import copy
+
 from PyQt5.QtCore import pyqtSignal, Qt, QAbstractItemModel, QModelIndex
 
 
@@ -227,6 +229,19 @@
         self.breakpoints.append(bp)
         self.endInsertRows()
     
+    def addBreakPoints(self, breakpoints):
+        """
+        Public method to add multiple breakpoints to the list.
+        
+        @param breakpoints list of breakpoints with file name, line number,
+            condition, temporary flag, enabled flag and ignore count each
+        @type list of (str, int, str, bool, bool, int)
+        """
+        cnt = len(self.breakpoints)
+        self.beginInsertRows(QModelIndex(), cnt, cnt + len(breakpoints) - 1)
+        self.breakpoints += breakpoints
+        self.endInsertRows()
+    
     def setBreakPointByIndex(self, index, fn, line, properties):
         """
         Public method to set the values of a breakpoint given by index.
@@ -321,7 +336,16 @@
             return self.breakpoints[index.row()][:]  # return a copy
         else:
             return []
-
+    
+    def getAllBreakpoints(self):
+        """
+        Public method to get a copy of the breakpoints.
+        
+        @return list of breakpoints
+        @rtype list of list of [str, int, str, bool, bool, int]
+        """
+        return copy.deepcopy(self.breakpoints)
+    
     def getBreakPointIndex(self, fn, lineno):
         """
         Public method to get the index of a breakpoint given by filename and
--- a/eric6/Debugger/DebugUI.py	Mon Jan 25 20:07:51 2021 +0100
+++ b/eric6/Debugger/DebugUI.py	Wed Jan 27 15:09:20 2021 +0100
@@ -1714,7 +1714,8 @@
                 # save the info for later use
                 self.project.setDbgInfo(
                     lastUsedVenvName, argv, wd, env, exceptions, self.excList,
-                    self.excIgnoreList, clearShell)
+                    self.excIgnoreList, clearShell
+                )
                 
                 self.lastStartAction = 6
                 self.clientType = self.project.getProjectLanguage()
@@ -1850,7 +1851,8 @@
                 # save the info for later use
                 self.project.setDbgInfo(
                     lastUsedVenvName, argv, wd, env, exceptions, self.excList,
-                    self.excIgnoreList, clearShell)
+                    self.excIgnoreList, clearShell
+                )
                 
                 self.lastStartAction = 8
                 self.clientType = self.project.getProjectLanguage()
@@ -1985,7 +1987,8 @@
                 # save the info for later use
                 self.project.setDbgInfo(
                     lastUsedVenvName, argv, wd, env, exceptions, self.excList,
-                    self.excIgnoreList, clearShell)
+                    self.excIgnoreList, clearShell
+                )
                 
                 self.lastStartAction = 4
                 self.clientType = self.project.getProjectLanguage()
@@ -2123,7 +2126,10 @@
                 self.project.setDbgInfo(
                     lastUsedVenvName, argv, wd, env, exceptions, self.excList,
                     self.excIgnoreList, clearShell, tracePython=tracePython,
-                    autoContinue=self.autoContinue)
+                    autoContinue=autoContinue,
+                    enableMultiprocess=enableMultiprocess,
+                    multiprocessNoDebug=multiprocessNoDebug
+                )
                 
                 self.lastStartAction = 2
                 self.clientType = self.project.getProjectLanguage()
@@ -2548,3 +2554,13 @@
             if noDebugList in self.multiprocessNoDebugHistory:
                 self.multiprocessNoDebugHistory.remove(noDebugList)
             self.multiprocessNoDebugHistory.insert(0, noDebugList)
+    
+    def setEnableMultiprocess(self, enableMultiprocess):
+        """
+        Public slot to initialize the enableMultiprocess flag.
+        
+        @param enableMultiprocess flag indicating, that the debugger should be
+            run in multi process mode
+        @type bool
+        """
+        self.enableMultiprocess = enableMultiprocess
--- a/eric6/Debugger/WatchPointModel.py	Mon Jan 25 20:07:51 2021 +0100
+++ b/eric6/Debugger/WatchPointModel.py	Wed Jan 27 15:09:20 2021 +0100
@@ -7,6 +7,8 @@
 Module implementing the Watch expression model.
 """
 
+import copy
+
 from PyQt5.QtCore import pyqtSignal, Qt, QAbstractItemModel, QModelIndex
 
 
@@ -169,11 +171,13 @@
         """
         Public method to add a new watch expression to the list.
         
-        @param cond expression of the watch expression (string)
-        @param special special condition of the watch expression (string)
+        @param cond expression of the watch expression
+        @type str
+        @param special special condition of the watch expression
+        @type str
         @param properties properties of the watch expression
-            (tuple of temporary flag (bool), enabled flag (bool),
-            ignore count (integer))
+            (tuple of temporary flag, enabled flag, ignore count)
+        @type tuple of (bool, bool, int)
         """
         wp = [cond, special] + list(properties)
         cnt = len(self.watchpoints)
@@ -181,6 +185,19 @@
         self.watchpoints.append(wp)
         self.endInsertRows()
     
+    def addWatchPoints(self, watchpoints):
+        """
+        Public method to add multiple watch expressions to the list.
+        
+        @param watchpoints list of watch expressions with expression, special
+            condition, temporary flag, enabled flag and ignore count each
+        @type list of (str, str, bool, bool, int)
+        """
+        cnt = len(self.watchpoints)
+        self.beginInsertRows(QModelIndex(), cnt, cnt + len(watchpoints) - 1)
+        self.watchpoints += watchpoints
+        self.endInsertRows()
+    
     def setWatchPointByIndex(self, index, cond, special, properties):
         """
         Public method to set the values of a watch expression given by index.
@@ -198,10 +215,7 @@
             index2 = self.createIndex(
                 row, len(self.watchpoints[row]), self.watchpoints[row])
             self.dataAboutToBeChanged.emit(index1, index2)
-            i = 0
-            for value in [cond, special] + list(properties):
-                self.watchpoints[row][i] = value
-                i += 1
+            self.watchpoints[row] = [cond, special] + list(properties)
             self.dataChanged.emit(index1, index2)
 
     def setWatchPointEnabledByIndex(self, index, enabled):
@@ -265,14 +279,23 @@
         
         @param index index of the watch expression (QModelIndex)
         @return watch expression (list of six values (expression,
-            special condition, temporary flag, enabled flag, ignore count,
-            index))
+            special condition, temporary flag, enabled flag, ignore count))
+        @rtype tuple of (str, str, bool, bool, int)
         """
         if index.isValid():
             return self.watchpoints[index.row()][:]  # return a copy
         else:
             return []
-
+    
+    def getAllWatchpoints(self):
+        """
+        Public method to get the list of watchpoints.
+        
+        @return list of watchpoints
+        @rtype list of list of [str, str, bool, bool, int]
+        """
+        return copy.deepcopy(self.watchpoints)
+    
     def getWatchPointIndex(self, cond, special=""):
         """
         Public method to get the index of a watch expression given by
--- a/eric6/E5XML/SessionWriter.py	Mon Jan 25 20:07:51 2021 +0100
+++ b/eric6/E5XML/SessionWriter.py	Wed Jan 27 15:09:20 2021 +0100
@@ -110,7 +110,7 @@
         self.writeEndElement()
         
         aw = self.vm.getActiveName()
-        if aw and self.project.isProjectFile(aw):
+        if aw and (isGlobal or self.project.isProjectFile(aw)):
             ed = self.vm.getOpenEditor(aw)
             line, index = ed.getCursorPosition()
             self.writeStartElement("ActiveWindow")
--- a/eric6/Project/Project.py	Mon Jan 25 20:07:51 2021 +0100
+++ b/eric6/Project/Project.py	Wed Jan 27 15:09:20 2021 +0100
@@ -45,6 +45,8 @@
 from .UserProjectFile import UserProjectFile
 from .DebuggerPropertiesFile import DebuggerPropertiesFile
 
+from Sessions.SessionFile import SessionFile
+
 
 class Project(QObject):
     """
@@ -191,6 +193,7 @@
         self.__projectFile = ProjectFile(self)
         self.__userProjectFile = UserProjectFile(self)
         self.__debuggerPropertiesFile = DebuggerPropertiesFile(self)
+        self.__sessionFile = SessionFile(False)
         
         self.recent = []
         self.__loadRecent()
@@ -447,6 +450,8 @@
         self.dbgAutoClearShell = True
         self.dbgTracePython = False
         self.dbgAutoContinue = True
+        self.dbgEnableMultiprocess = True
+        self.dbgMultiprocessNoDebug = ""
         
         self.pdata = {
             "DESCRIPTION": "",
@@ -950,15 +955,15 @@
         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))
+        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):
             # try the new JSON based format first
             self.__userProjectFile.readFile(fn)
         else:
             # try the old XML based format second
             fn = os.path.join(self.getProjectManagementDir(),
-                              '{0}.e4q'.format(fn))
+                              '{0}.e4q'.format(fn1))
             if os.path.exists(fn):
                 f = QFile(fn)
                 if f.open(QIODevice.ReadOnly):
@@ -997,9 +1002,9 @@
         else:
             fn, ext = os.path.splitext(os.path.basename(self.pfile))
             fn_new = os.path.join(self.getProjectManagementDir(),
-                                  '{0}.e5s'.format(fn))
+                                  '{0}.esj'.format(fn))
             fn_old = os.path.join(self.getProjectManagementDir(),
-                                  '{0}.e4s'.format(fn))
+                                  '{0}.e5s'.format(fn))
             enable = os.path.exists(fn_new) or os.path.exists(fn_old)
         self.sessActGrp.findChild(
             QAction, "project_load_session").setEnabled(enable)
@@ -1009,50 +1014,53 @@
     @pyqtSlot()
     def __readSession(self, quiet=False, indicator=""):
         """
-        Private method to read in the project session file (.e5s or .e4s).
+        Private method to read in the project session file (.esj or .e5s).
         
         @param quiet flag indicating quiet operations.
                 If this flag is true, no errors are reported.
         @param indicator indicator string (string)
         """
-        # TODO: read project session
         if self.pfile is None:
             if not quiet:
                 E5MessageBox.critical(
                     self.ui,
-                    self.tr("Read project session"),
+                    self.tr("Read Project Session"),
                     self.tr("Please save the project first."))
             return
             
         fn1, ext = os.path.splitext(os.path.basename(self.pfile))
         fn = os.path.join(self.getProjectManagementDir(),
-                          '{0}{1}.e5s'.format(fn1, indicator))
-        if not os.path.exists(fn):
+                          '{0}{1}.esj'.format(fn1, indicator))
+        if os.path.exists(fn):
+            # try the new JSON based format first
+            self.__sessionFile.readFile(fn)
+        else:
+            # try the old XML based format second
             fn = os.path.join(self.getProjectManagementDir(),
-                              '{0}{1}.e4s'.format(fn1, indicator))
-        
-        f = QFile(fn)
-        if f.open(QIODevice.ReadOnly):
-            from E5XML.SessionReader import SessionReader
-            reader = SessionReader(f, False)
-            reader.readXML(quiet=quiet)
-            f.close()
-        else:
-            if not quiet:
-                E5MessageBox.critical(
-                    self.ui,
-                    self.tr("Read project session"),
-                    self.tr(
-                        "<p>The project session file <b>{0}</b> could not be"
-                        " read.</p>").format(fn))
+                              '{0}{1}.e5s'.format(fn1, indicator))
+            if os.path.exists(fn):
+                f = QFile(fn)
+                if f.open(QIODevice.ReadOnly):
+                    from E5XML.SessionReader import SessionReader
+                    reader = SessionReader(f, False)
+                    reader.readXML(quiet=quiet)
+                    f.close()
+                else:
+                    if not quiet:
+                        E5MessageBox.critical(
+                            self.ui,
+                            self.tr("Read project session"),
+                            self.tr(
+                                "<p>The project session file <b>{0}</b> could"
+                                " not be read.</p>").format(fn))
         
     @pyqtSlot()
     def __writeSession(self, quiet=False, indicator=""):
         """
-        Private method to write the session data to an XML file (.e5s).
+        Private method to write the session data to an XML file (.esj).
         
         @param quiet flag indicating quiet operations.
-                If this flag is true, no errors are reported.
+            If this flag is true, no errors are reported.
         @param indicator indicator string (string)
         """
         # TODO: write project session
@@ -1060,29 +1068,16 @@
             if not quiet:
                 E5MessageBox.critical(
                     self.ui,
-                    self.tr("Save project session"),
+                    self.tr("Save Project Session"),
                     self.tr("Please save the project first."))
             return
         
         fn, ext = os.path.splitext(os.path.basename(self.pfile))
         fn = os.path.join(self.getProjectManagementDir(),
-                          '{0}{1}.e5s'.format(fn, indicator))
-        
-        f = QFile(fn)
-        if f.open(QIODevice.WriteOnly):
-            from E5XML.SessionWriter import SessionWriter
-            SessionWriter(
-                f, os.path.splitext(os.path.basename(fn))[0]).writeXML()
-            f.close()
-        else:
-            if not quiet:
-                E5MessageBox.critical(
-                    self.ui,
-                    self.tr("Save project session"),
-                    self.tr(
-                        "<p>The project session file <b>{0}</b> could not be"
-                        " written.</p>").format(fn))
-        
+                          '{0}{1}.esj'.format(fn, indicator))
+        
+        self.__sessionFile.writeFile(fn)
+    
     def __deleteSession(self):
         """
         Private method to delete the session file.
@@ -1206,16 +1201,16 @@
                     self.tr("Please save the project first."))
             return
         
-        fn, ext = os.path.splitext(os.path.basename(self.pfile))
-        fn = os.path.join(self.getProjectManagementDir(), '{0}.edj'.format(fn))
+        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):
             # try the new JSON based format first
             self.__debuggerPropertiesFile.readFile(fn)
         else:
             # try the old XML based format second
-            fn, ext = os.path.splitext(os.path.basename(self.pfile))
             fn = os.path.join(self.getProjectManagementDir(),
-                              '{0}.e4d'.format(fn))
+                              '{0}.e4d'.format(fn1))
             
             f = QFile(fn)
             if f.open(QIODevice.ReadOnly):
@@ -1344,7 +1339,8 @@
     
     def setDbgInfo(self, venvName, argv, wd, env, excReporting, excList,
                    excIgnoreList, autoClearShell, tracePython=None,
-                   autoContinue=None):
+                   autoContinue=None, enableMultiprocess=None,
+                   multiprocessNoDebug=None):
         """
         Public method to set the debugging information.
         
@@ -1371,6 +1367,12 @@
         @param autoContinue flag indicating, that the debugger should not
             stop at the first executable line
         @type bool
+        @param enableMultiprocess flag indicating, that the debugger should
+            run in multi process mode
+        @type bool
+        @param multiprocessNoDebug list of programs not to be debugged in
+            multi process mode
+        @type str
         """
         self.dbgVirtualEnv = venvName
         self.dbgCmdline = argv
@@ -1384,6 +1386,10 @@
             self.dbgTracePython = tracePython
         if autoContinue is not None:
             self.dbgAutoContinue = autoContinue
+        if enableMultiprocess is not None:
+            self.dbgEnableMultiprocess = enableMultiprocess
+        if multiprocessNoDebug is not None:
+            self.dbgMultiprocessNoDebug = multiprocessNoDebug
     
     def getTranslationPattern(self):
         """
--- a/eric6/QScintilla/Lexers/__init__.py	Mon Jan 25 20:07:51 2021 +0100
+++ b/eric6/QScintilla/Lexers/__init__.py	Wed Jan 27 15:09:20 2021 +0100
@@ -863,6 +863,13 @@
         '*.e6q': "XML",
         '*.e6s': "XML",
         '*.e6t': "XML",
+        '*.edj': "JSON",
+        '*.ekj': "JSON",
+        '*.emj': "JSON",
+        '*.epj': "JSON",
+        '*.eqj': "JSON",
+        '*.esj': "JSON",
+        '*.etj': "JSON",
         '*.proto': "Protocol",
         '*.po': "Gettext",
         '*.coffee': "CoffeeScript",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Sessions/SessionFile.py	Wed Jan 27 15:09:20 2021 +0100
@@ -0,0 +1,369 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a class representing the project debugger properties
+JSON file.
+"""
+
+import json
+import os
+import time
+
+from PyQt5.QtCore import QObject
+
+from E5Gui import E5MessageBox
+from E5Gui.E5OverrideCursor import E5OverridenCursor
+from E5Gui.E5Application import e5App
+
+import Preferences
+
+
+class SessionFile(QObject):
+    """
+    Class representing the project debugger properties JSON file.
+    """
+    def __init__(self, isGlobal: bool, parent: QObject = None):
+        """
+        Constructor
+        
+        @param project reference to the project object
+        @type Project
+        @param parent reference to the parent object
+        @type QObject (optional)
+        """
+        super(SessionFile, self).__init__(parent)
+        
+        self.__isGlobal = isGlobal
+    
+    def writeFile(self, filename: str) -> bool:
+        """
+        Public method to write the project debugger properties data to a
+        project debugger properties JSON file.
+        
+        @param filename name of the user project file
+        @type str
+        @return flag indicating a successful write
+        @rtype bool
+        """
+        # get references to objects we need
+        project = e5App().getObject("Project")
+        projectBrowser = e5App().getObject("ProjectBrowser")
+        multiProject = e5App().getObject("MultiProject")
+        vm = e5App().getObject("ViewManager")
+        dbg = e5App().getObject("DebugUI")
+        dbs = e5App().getObject("DebugServer")
+        
+        name = os.path.splitext(os.path.basename(filename))[0]
+        
+        # prepare the session data dictionary
+        # step 0: header
+        sessionDict = {}
+        sessionDict["header"] = {}
+        if not self.__isGlobal:
+            sessionDict["header"]["comment"] = (
+                f"eric session file for project {name}"
+        )
+        sessionDict["header"]["warning"] = (
+            "This file was generated automatically, do not edit."
+        )
+        
+        # TODO: replace 'XMLTimestamp' by 'Timestamp'
+        if Preferences.getProject("XMLTimestamp") or self.__isGlobal:
+            sessionDict["header"]["saved"] = (
+                time.strftime('%Y-%m-%d, %H:%M:%S')
+            )
+        
+        # step 1: open multi project and project for global session
+        sessionDict["MultiProject"] = ""
+        sessionDict["Project"] = ""
+        if self.__isGlobal:
+            if multiProject.isOpen():
+                sessionDict["MultiProject"] = (
+                    multiProject.getMultiProjectFile()
+                )
+            if project.isOpen():
+                sessionDict["Project"] = project.getProjectFile()
+        
+        # step 2: all open (project) filenames and the active editor
+        if vm.canSplit():
+            sessionDict["ViewManagerSplits"] = {
+                "Count": vm.splitCount(),
+                "Orientation": vm.getSplitOrientation(),
+            }
+        else:
+            sessionDict["ViewManagerSplits"] = {
+                "Count": 0,
+                "Orientation": 1,
+            }
+        
+        editorsDict = {}    # remember editors by file name to detect clones
+        sessionDict["Editors"] = []
+        allOpenEditorLists = vm.getOpenEditorsForSession()
+        for splitIndex, openEditorList in enumerate(allOpenEditorLists):
+            for editorIndex, editor in enumerate(openEditorList):
+                fileName = editor.getFileName()
+                if self.__isGlobal or project.isProjectFile(fileName):
+                    if fileName in editorsDict:
+                        isClone = editorsDict[fileName].isClone(editor)
+                    else:
+                        isClone = False
+                        editorsDict[fileName] = editor
+                    editorDict = {
+                        "Filename": fileName,
+                        "Cursor": editor.getCursorPosition(),
+                        "Folds": editor.contractedFolds(),
+                        "Zoom": editor.getZoom(),
+                        "Clone": isClone,
+                        "Splitindex": splitIndex,
+                        "Editorindex": editorIndex,
+                    }
+                    sessionDict["Editors"].append(editorDict)
+        
+        aw = vm.getActiveName()
+        sessionDict["ActiveWindow"] = {}
+        if aw and (self.__isGlobal or project.isProjectFile(aw)):
+            ed = vm.getOpenEditor(aw)
+            sessionDict["ActiveWindow"] = {
+                "Filename": aw,
+                "Cursor": ed.getCursorPosition(),
+            }
+        
+        # step 3: breakpoints
+        allBreaks = Preferences.getProject("SessionAllBreakpoints")
+        projectFiles = project.getSources(True)
+        bpModel = dbs.getBreakPointModel()
+        if self.__isGlobal or allBreaks:
+            sessionDict["Breakpoints"] = bpModel.getAllBreakpoints()
+        else:
+            sessionDict["Breakpoints"] = [
+                bp
+                for bp in bpModel.getAllBreakpoints()
+                if bp[0] in projectFiles
+            ]
+        
+        # step 4: watch expressions
+        wpModel = dbs.getWatchPointModel()
+        sessionDict["Watchpoints"] = wpModel.getAllWatchpoints()
+        
+        # step 5: debug info
+        if self.__isGlobal:
+            if len(dbg.argvHistory):
+                dbgCmdline = dbg.argvHistory[0]
+            else:
+                dbgCmdline = ""
+            if len(dbg.wdHistory):
+                dbgWd = dbg.wdHistory[0]
+            else:
+                dbgWd = ""
+            if len(dbg.envHistory):
+                dbgEnv = dbg.envHistory[0]
+            else:
+                dbgEnv = ""
+            if len(dbg.multiprocessNoDebugHistory):
+                dbgMultiprocessNoDebug = (
+                    dbg.multiprocessNoDebugHistory[0]
+                )
+            else:
+                dbgMultiprocessNoDebug = ""
+            sessionDict["DebugInfo"] = {
+                "VirtualEnv": dbg.lastUsedVenvName,
+                "CommandLine": dbgCmdline,
+                "WorkingDirectory": dbgWd,
+                "Environment": dbgEnv,
+                "ReportExceptions": dbg.exceptions,
+                "Exceptions": dbg.excList,
+                "IgnoredExceptions": dbg.excIgnoreList,
+                "AutoClearShell": dbg.autoClearShell,
+                "TracePython": dbg.tracePython,
+                "AutoContinue": dbg.autoContinue,
+                "EnableMultiprocess": dbg.enableMultiprocess,
+                "MultiprocessNoDebug": dbgMultiprocessNoDebug
+            }
+        else:
+            sessionDict["DebugInfo"] = {
+                "VirtualEnv": project.dbgVirtualEnv,
+                "CommandLine": project.dbgCmdline,
+                "WorkingDirectory": project.dbgWd,
+                "Environment": project.dbgEnv,
+                "ReportExceptions": project.dbgReportExceptions,
+                "Exceptions": project.dbgExcList,
+                "IgnoredExceptions": project.dbgExcIgnoreList,
+                "AutoClearShell": project.dbgAutoClearShell,
+                "TracePython": project.dbgTracePython,
+                "AutoContinue": project.dbgAutoContinue,
+                "EnableMultiprocess": project.dbgEnableMultiprocess,
+                "MultiprocessNoDebug": project.dbgMultiprocessNoDebug,
+            }
+        
+        # step 6: bookmarks
+        bookmarksList = []
+        for fileName in editorsDict:
+            if self.__isGlobal or project.isProjectFile(fileName):
+                editor = editorsDict[fileName]
+                bookmarks = editor.getBookmarks()
+                if bookmarks:
+                    bookmarksList.append({
+                        "Filename": fileName,
+                        "Lines": bookmarks,
+                    })
+        sessionDict["Bookmarks"] = bookmarksList
+        
+        # step 7: state of the various project browsers
+        browsersList = []
+        for browserName in projectBrowser.getProjectBrowserNames():
+            expandedItems = (
+                projectBrowser.getProjectBrowser(browserName)
+                .getExpandedItemNames()
+            )
+            if expandedItems:
+                browsersList.append({
+                    "Name": browserName,
+                    "ExpandedItems": expandedItems,
+                })
+        sessionDict["ProjectBrowserStates"] = browsersList
+        
+        try:
+            jsonString = json.dumps(sessionDict, indent=2)
+            with open(filename, "w") as f:
+                f.write(jsonString)
+        except (TypeError, EnvironmentError) as err:
+            with E5OverridenCursor():
+                E5MessageBox.critical(
+                    None,
+                    self.tr("Save Session"),
+                    self.tr(
+                        "<p>The session file <b>{0}</b> could not be"
+                        " written.</p><p>Reason: {1}</p>"
+                    ).format(filename, str(err))
+                )
+                return False
+        
+        return True
+    
+    def readFile(self, filename: str) -> bool:
+        """
+        Public method to read the session data from a session JSON file.
+        
+        @param filename name of the project file
+        @type str
+        @return flag indicating a successful read
+        @rtype bool
+        """
+        try:
+            with open(filename, "r") as f:
+                jsonString = f.read()
+            sessionDict = json.loads(jsonString)
+        except (EnvironmentError, json.JSONDecodeError) as err:
+            E5MessageBox.critical(
+                None,
+                self.tr("Read Debugger Properties"),
+                self.tr(
+                    "<p>The project debugger properties file <b>{0}</b>"
+                    " could not be read.</p><p>Reason: {1}</p>"
+                ).format(filename, str(err))
+            )
+            return False
+        
+        # get references to objects we need
+        project = e5App().getObject("Project")
+        projectBrowser = e5App().getObject("ProjectBrowser")
+        multiProject = e5App().getObject("MultiProject")
+        vm = e5App().getObject("ViewManager")
+        dbg = e5App().getObject("DebugUI")
+        dbs = e5App().getObject("DebugServer")
+        
+        # step 1: multi project and project
+        if sessionDict["MultiProject"]:
+            multiProject.openMultiProject(sessionDict["MultiProject"], False)
+        if sessionDict["Project"]:
+            project.openProject(sessionDict["Project"], False)
+        
+        # step 2: (project) filenames and the active editor
+        vm.setSplitOrientation(sessionDict["ViewManagerSplits"]["Orientation"])
+        vm.setSplitCount(sessionDict["ViewManagerSplits"]["Count"])
+        
+        editorsDict = {}
+        for editorDict in sessionDict["Editors"]:
+            if editorDict["Clone"] and editorDict["Filename"] in editorsDict:
+                editor = editorsDict[editorDict["Filename"]]
+                ed = vm.newEditorView(
+                    editorDict["Filename"], editor, editor.getFileType(),
+                    indexes=(editorDict["Splitindex"],
+                             editorDict["Editorindex"])
+                )
+            else:
+                ed = vm.openSourceFile(
+                    editorDict["Filename"],
+                    indexes=(editorDict["Splitindex"],
+                             editorDict["Editorindex"])
+                )
+                editorsDict[editorDict["Filename"]] = ed
+            if ed is not None:
+                ed.zoomTo(editorDict["Zoom"])
+                if editorDict["Folds"]:
+                    ed.recolor()
+                    ed.setContractedFolds(editorDict["Folds"])
+                    ed.setCursorPosition(*editorDict["Cursor"])
+        
+        # step 3: breakpoints
+        bpModel = dbs.getBreakPointModel()
+        bpModel.addBreakPoints(sessionDict["Breakpoints"])
+        
+        # step 4: watch expressions
+        wpModel = dbs.getWatchPointModel()
+        wpModel.addWatchPoints(sessionDict["Watchpoints"])
+        
+        # step 5: debug info
+        debugInfoDict = sessionDict["DebugInfo"]
+        dbg.lastUsedVenvName = debugInfoDict["VirtualEnv"]
+        dbg.setArgvHistory(debugInfoDict["CommandLine"])
+        dbg.setWdHistory(debugInfoDict["WorkingDirectory"])
+        dbg.setEnvHistory(debugInfoDict["Environment"])
+        dbg.setExceptionReporting(debugInfoDict["ReportExceptions"])
+        dbg.setExcList(debugInfoDict["Exceptions"])
+        dbg.setExcIgnoreList(debugInfoDict["IgnoredExceptions"])
+        dbg.setAutoClearShell(debugInfoDict["AutoClearShell"])
+        dbg.setTracePython(debugInfoDict["TracePython"])
+        dbg.setAutoContinue(debugInfoDict["AutoContinue"])
+        dbg.setEnableMultiprocess(debugInfoDict["EnableMultiprocess"])
+        dbg.setMultiprocessNoDebugHistory(debugInfoDict["MultiprocessNoDebug"])
+        if not self.__isGlobal:
+            project.setDbgInfo(
+                debugInfoDict["VirtualEnv"],
+                debugInfoDict["CommandLine"],
+                debugInfoDict["WorkingDirectory"],
+                debugInfoDict["Environment"],
+                debugInfoDict["ReportExceptions"],
+                debugInfoDict["Exceptions"],
+                debugInfoDict["IgnoredExceptions"],
+                debugInfoDict["AutoClearShell"],
+                debugInfoDict["TracePython"],
+                debugInfoDict["AutoContinue"],
+                debugInfoDict["EnableMultiprocess"],
+                debugInfoDict["MultiprocessNoDebug"]
+            )
+        
+        # step 6: bookmarks
+        for bookmark in sessionDict["Bookmarks"]:
+            editor = vm.getOpenEditor(bookmark["Filename"])
+            if editor is not None:
+                for lineno in bookmark["Lines"]:
+                    editor.toggleBookmark(lineno)
+        
+        # step 7: state of the various project browsers
+        for browserState in sessionDict["ProjectBrowserStates"]:
+            browser = projectBrowser.getProjectBrowser(browserState["Name"])
+            if browser is not None:
+                browser.expandItemsByName(browserState["ExpandedItems"])
+        
+        # step 8: active window
+        vm.openFiles(sessionDict["ActiveWindow"]["Filename"])
+        ed = vm.getOpenEditor(sessionDict["ActiveWindow"]["Filename"])
+        if ed is not None:
+            ed.setCursorPosition(*sessionDict["ActiveWindow"]["Cursor"])
+            ed.ensureCursorVisible()
+        
+        return True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Sessions/__init__.py	Wed Jan 27 15:09:20 2021 +0100
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Package containing session related modules.
+"""
--- a/eric6/UI/UserInterface.py	Mon Jan 25 20:07:51 2021 +0100
+++ b/eric6/UI/UserInterface.py	Wed Jan 27 15:09:20 2021 +0100
@@ -51,6 +51,8 @@
 
 import UI.PixmapCache
 
+from Sessions.SessionFile import SessionFile
+
 from E5Network.E5NetworkProxyFactory import (
     E5NetworkProxyFactory, proxyAuthenticationRequired
 )
@@ -610,6 +612,9 @@
         self.__inVersionCheck = False
         self.__versionCheckProgress = None
         
+        # create the various JSON file interfaces
+        self.__sessionFile = SessionFile(True)
+        
         # Initialize the actions, menus, toolbars and statusbar
         splash.showMessage(self.tr("Initializing Actions..."))
         self.__initActions()
@@ -6516,7 +6521,7 @@
         
     def __writeSession(self, filename="", crashSession=False):
         """
-        Private slot to write the session data to an XML file (.e5s).
+        Private slot to write the session data to a JSON file (.esj).
         
         @param filename name of a session file to write
         @type str
@@ -6525,33 +6530,38 @@
         @return flag indicating success
         @rtype bool
         """
-        res = False
         if filename:
             fn = filename
         elif crashSession:
             fn = os.path.join(Utilities.getConfigDir(),
-                              "eric6_crash_session.e5s")
+                              "eric6_crash_session.esj")
         else:
             fn = os.path.join(Utilities.getConfigDir(),
-                              "eric6session.e5s")
-        f = QFile(fn)
-        if f.open(QIODevice.WriteOnly):
-            from E5XML.SessionWriter import SessionWriter
-            SessionWriter(f, None).writeXML()
-            f.close()
-            res = True
+                              "eric6session.esj")
+        
+        if fn.endswith(".esj"):
+            res = self.__sessionFile.writeFile(fn)
         else:
-            E5MessageBox.critical(
-                self,
-                self.tr("Save session"),
-                self.tr("<p>The session file <b>{0}</b> could not be"
-                        " written.</p>")
-                .format(fn))
+            f = QFile(fn)
+            if f.open(QIODevice.WriteOnly):
+                from E5XML.SessionWriter import SessionWriter
+                SessionWriter(f, None).writeXML()
+                f.close()
+                res = True
+            else:
+                E5MessageBox.critical(
+                    self,
+                    self.tr("Save Session"),
+                    self.tr("<p>The session file <b>{0}</b> could not be"
+                            " written.</p>")
+                    .format(fn))
+                res = False
+        
         return res
-        
+    
     def __readSession(self, filename=""):
         """
-        Private slot to read in the session file (.e5s or .e4s).
+        Private slot to read in the session file (.esj or .e5s).
         
         @param filename name of a session file to read
         @type str
@@ -6562,14 +6572,14 @@
             fn = filename
         else:
             fn = os.path.join(Utilities.getConfigDir(),
-                              "eric6session.e5s")
+                              "eric6session.esj")
             if not os.path.exists(fn):
                 fn = os.path.join(Utilities.getConfigDir(),
-                                  "eric6session.e4s")
+                                  "eric6session.e5s")
                 if not os.path.exists(fn):
                     E5MessageBox.critical(
                         self,
-                        self.tr("Read session"),
+                        self.tr("Read Session"),
                         self.tr("<p>The session file <b>{0}</b> could not"
                                 " be read.</p>")
                         .format(fn))
@@ -6577,22 +6587,29 @@
         
         res = False
         if fn:
-            f = QFile(fn)
-            if f.open(QIODevice.ReadOnly):
-                from E5XML.SessionReader import SessionReader
+            if fn.endswith(".esj"):
+                # new JSON based format
                 self.__readingSession = True
-                reader = SessionReader(f, True)
-                reader.readXML()
+                res = self.__sessionFile.readFile(fn)
                 self.__readingSession = False
-                f.close()
-                res = True
             else:
-                E5MessageBox.critical(
-                    self,
-                    self.tr("Read session"),
-                    self.tr("<p>The session file <b>{0}</b> could not be"
-                            " read.</p>")
-                    .format(fn))
+                # old XML based format
+                f = QFile(fn)
+                if f.open(QIODevice.ReadOnly):
+                    from E5XML.SessionReader import SessionReader
+                    self.__readingSession = True
+                    reader = SessionReader(f, True)
+                    reader.readXML()
+                    self.__readingSession = False
+                    f.close()
+                    res = True
+                else:
+                    E5MessageBox.critical(
+                        self,
+                        self.tr("Read session"),
+                        self.tr("<p>The session file <b>{0}</b> could not be"
+                                " read.</p>")
+                        .format(fn))
         
         # Write a crash session after a session was read.
         self.__writeCrashSession()
@@ -6605,9 +6622,10 @@
         """
         sessionFile, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
             self,
-            self.tr("Save session"),
+            self.tr("Save Session"),
             Utilities.getHomeDir(),
-            self.tr("eric Session Files (*.e5s)"),
+            self.tr("eric Session Files (*.esj);;"
+                    "eric XML Session Files (*.e5s)"),
             "")
         
         if not sessionFile:
@@ -6629,7 +6647,8 @@
             self,
             self.tr("Load session"),
             Utilities.getHomeDir(),
-            self.tr("eric Session Files (*.e5s)"))
+            self.tr("eric Session Files (*.esj);;"
+                    "eric XML Session Files (*.e5s)"))
         
         if not sessionFile:
             return
@@ -6640,14 +6659,15 @@
         """
         Private slot to delete the crash session file.
         """
-        fn = os.path.join(Utilities.getConfigDir(),
-                          "eric6_crash_session.e5s")
-        if os.path.exists(fn):
-            try:
-                os.remove(fn)
-            except OSError:
-                # ignore it silently
-                pass
+        for ext in (".esj", ".e5s"):
+            fn = os.path.join(Utilities.getConfigDir(),
+                              f"eric6_crash_session{ext}")
+            if os.path.exists(fn):
+                try:
+                    os.remove(fn)
+                except OSError:
+                    # ignore it silently
+                    pass
     
     def __writeCrashSession(self):
         """
@@ -6674,7 +6694,7 @@
             Preferences.getUI("OpenCrashSessionOnStartup")
         ):
             fn = os.path.join(Utilities.getConfigDir(),
-                              "eric6_crash_session.e5s")
+                              "eric6_crash_session.esj")
             if os.path.exists(fn):
                 yes = E5MessageBox.yesNo(
                     self,

eric ide

mercurial