From bbbc209939a2dd726b3da5a12152b4e4d60fe8df Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Wed, 13 Dec 2017 18:45:08 -0800 Subject: [PATCH 01/39] Create background thread to constantly check the file stats of a given file. Need to integrate this into the program --- app/file_stats.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++ app/mutator.py | 20 ++------------- 2 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 app/file_stats.py diff --git a/app/file_stats.py b/app/file_stats.py new file mode 100644 index 00000000..a8c9029f --- /dev/null +++ b/app/file_stats.py @@ -0,0 +1,62 @@ +import os +import time +import threading + +def FileStats: + def __init__(self, fullPath, pollingInterval=1): + """ + Args: + fullPath (str): The absolute path of the file you want to keep track of. + pollingInterval (int): The frequency at which you want to poll the file. + """ + self.pollingInterval = pollingInterval + self.fullPath = fullPath + self.fileStats = None + thread = threading.Thread(target=self.run) + + def run(self): + try: + while True: + self.fileStats = os.stat(self.fullPath) + time.sleep(self.interval) + except Exception as e: + app.log.info("Exception occurred while running file stats thread:", e) + + def getFileSize(self): + if self.fileStats: + return self.fileStats.st_size + else: + return 0 + + def getFileStats(): + return self.fileStats + + def fileChanged(self): + """ + Compares the file's stats with the recorded stats we have in memory. + + Args: + None. + + Returns: + The new file stats if the file has changed. Otherwise, None. + """ + s1 = self.fileStats + s2 = os.stat(self.fullPath) + app.log.info('st_mode', s1.st_mode, s2.st_mode) + app.log.info('st_ino', s1.st_ino, s2.st_ino) + app.log.info('st_dev', s1.st_dev, s2.st_dev) + app.log.info('st_uid', s1.st_uid, s2.st_uid) + app.log.info('st_gid', s1.st_gid, s2.st_gid) + app.log.info('st_size', s1.st_size, s2.st_size) + app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) + app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) + if (s1.st_mode == s2.st_mode and + s1.st_ino == s2.st_ino and + s1.st_dev == s2.st_dev and + s1.st_uid == s2.st_uid and + s1.st_gid == s2.st_gid and + s1.st_size == s2.st_size and + s1.st_mtime == s2.st_mtime and + s1.st_ctime == s2.st_ctime): + return s2 \ No newline at end of file diff --git a/app/mutator.py b/app/mutator.py index 018a3e81..43aa6bb8 100644 --- a/app/mutator.py +++ b/app/mutator.py @@ -139,24 +139,8 @@ def isDirty(self): def isSafeToWrite(self): if not os.path.exists(self.fullPath): return True - s1 = os.stat(self.fullPath) - s2 = self.fileStat - app.log.info('st_mode', s1.st_mode, s2.st_mode) - app.log.info('st_ino', s1.st_ino, s2.st_ino) - app.log.info('st_dev', s1.st_dev, s2.st_dev) - app.log.info('st_uid', s1.st_uid, s2.st_uid) - app.log.info('st_gid', s1.st_gid, s2.st_gid) - app.log.info('st_size', s1.st_size, s2.st_size) - app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) - app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) - return (s1.st_mode == s2.st_mode and - s1.st_ino == s2.st_ino and - s1.st_dev == s2.st_dev and - s1.st_uid == s2.st_uid and - s1.st_gid == s2.st_gid and - s1.st_size == s2.st_size and - s1.st_mtime == s2.st_mtime and - s1.st_ctime == s2.st_ctime) + #CALL THE FILESTATS OBJECT HERE + def __doMoveLines(self, begin, end, to): lines = self.lines[begin:end] From f27975c2fae7a7636eef4891f5b5ccd5bf257033 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Wed, 13 Dec 2017 19:31:32 -0800 Subject: [PATCH 02/39] Changed all references to self.isReadOnly to use the FileStats object --- app/actions.py | 7 +--- app/file_stats.py | 82 +++++++++++++++++++++++++--------------------- app/mutator.py | 4 +-- app/text_buffer.py | 2 ++ app/window.py | 2 +- 5 files changed, 51 insertions(+), 46 deletions(-) diff --git a/app/actions.py b/app/actions.py index 12828c7a..b03c3093 100644 --- a/app/actions.py +++ b/app/actions.py @@ -783,8 +783,6 @@ def setFilePath(self, path): def fileLoad(self): app.log.info('fileLoad', self.fullPath) inputFile = None - self.isReadOnly = (os.path.isfile(self.fullPath) and - not os.access(self.fullPath, os.W_OK)) if not os.path.exists(self.fullPath): self.setMessage('Creating new file') else: @@ -807,7 +805,6 @@ def fileLoad(self): app.log.info('error opening file', self.fullPath) self.setMessage('error opening file', self.fullPath) return - self.fileStat = os.stat(self.fullPath) self.relativePath = os.path.relpath(self.fullPath, os.getcwd()) app.log.info('fullPath', self.fullPath) app.log.info('cwd', os.getcwd()) @@ -959,7 +956,6 @@ def linesToData(self): def fileWrite(self): # Preload the message with an error that should be overwritten. self.setMessage('Error saving file') - self.isReadOnly = not os.access(self.fullPath, os.W_OK) try: try: if app.prefs.editor['onSaveStripTrailingSpaces']: @@ -991,11 +987,10 @@ def fileWrite(self): # Store the file's new info self.lastChecksum, self.lastFileSize = app.history.getFileInfo( self.fullPath) - self.fileStat = os.stat(self.fullPath) self.setMessage('File saved') except Exception as e: color = app.color.get('status_line_error') - if self.isReadOnly: + if self.fileStats.fileIsReadOnly(): self.setMessage("Permission error. Try modifing in sudo mode.", color=color) else: diff --git a/app/file_stats.py b/app/file_stats.py index a8c9029f..9aee953b 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -2,45 +2,53 @@ import time import threading -def FileStats: +class FileStats: def __init__(self, fullPath, pollingInterval=1): """ - Args: - fullPath (str): The absolute path of the file you want to keep track of. - pollingInterval (int): The frequency at which you want to poll the file. - """ - self.pollingInterval = pollingInterval - self.fullPath = fullPath - self.fileStats = None - thread = threading.Thread(target=self.run) - - def run(self): - try: - while True: - self.fileStats = os.stat(self.fullPath) - time.sleep(self.interval) - except Exception as e: - app.log.info("Exception occurred while running file stats thread:", e) - - def getFileSize(self): - if self.fileStats: - return self.fileStats.st_size - else: - return 0 - - def getFileStats(): - return self.fileStats - - def fileChanged(self): - """ - Compares the file's stats with the recorded stats we have in memory. - - Args: - None. - - Returns: - The new file stats if the file has changed. Otherwise, None. - """ + Args: + fullPath (str): The absolute path of the file you want to keep track of. + pollingInterval (int): The frequency at which you want to poll the file. + """ + self.pollingInterval = pollingInterval + self.fullPath = fullPath + self.fileStats = None + self.lock = threading.Lock() + self.isReadOnly = True + thread = threading.Thread(target=self.run) + + def run(self): + try: + while True: + self.lock.acquire() + self.fileStats = os.stat(self.fullPath) + self.isReadOnly = os.access(self.fullPath, os.W_OK) + self.lock.release() + time.sleep(self.interval) + except Exception as e: + app.log.info("Exception occurred while running file stats thread:", e) + + def getFileSize(self): + if self.fileStats: + return self.fileStats.st_size + else: + return 0 + + def getFileStats(self): + return self.fileStats + + def fileIsReadOnly(self): + return self.isReadOnly + + def fileChanged(self): + """ + Compares the file's stats with the recorded stats we have in memory. + + Args: + None. + + Returns: + The new file stats if the file has changed. Otherwise, None. + """ s1 = self.fileStats s2 = os.stat(self.fullPath) app.log.info('st_mode', s1.st_mode, s2.st_mode) diff --git a/app/mutator.py b/app/mutator.py index 43aa6bb8..e9c11ad4 100644 --- a/app/mutator.py +++ b/app/mutator.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import app.file_stats import app.log import app.parser import app.prefs @@ -44,9 +45,8 @@ def __init__(self): self.findBackRe = None self.fileExtension = '' self.fullPath = '' - self.fileStat = None + self.fileStats = app.file_stats.FileStats(self.fullPath, pollingInterval=2) self.goalCol = 0 - self.isReadOnly = False self.penGrammar = None self.parser = None self.parserTime = .0 diff --git a/app/text_buffer.py b/app/text_buffer.py index ad8f3987..2d1cd55c 100644 --- a/app/text_buffer.py +++ b/app/text_buffer.py @@ -14,6 +14,7 @@ import app.actions import app.color +import app.file_stats import app.log import app.parser import app.prefs @@ -34,6 +35,7 @@ def __init__(self): self.fileEncoding = None self.lastChecksum = None self.lastFileSize = 0 + self.fileStats = app.file_stats.FileStats(self.fullPath, pollingInterval=2) self.bookmarks = [] self.nextBookmarkColorPos = 0 diff --git a/app/window.py b/app/window.py index 5fb0b1e7..e5865479 100755 --- a/app/window.py +++ b/app/window.py @@ -627,7 +627,7 @@ def onChange(self): lineCursor -= 1 pathLine = self.host.textBuffer.fullPath if 1: - if tb.isReadOnly: + if tb.fileStats.fileIsReadOnly(): pathLine += ' [RO]' if 1: if tb.isDirty(): From bde81a99a2d43ae1d352ce742c10d58ea0b9c292 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 14 Dec 2017 11:11:59 -0800 Subject: [PATCH 03/39] Modified program to use the FileStats object --- app/actions.py | 10 ++--- app/buffer_manager.py | 2 + app/file_stats.py | 91 ++++++++++++++++++++++++++++++++++++++----- app/history.py | 66 +++++++++++++------------------ app/mutator.py | 2 +- 5 files changed, 115 insertions(+), 56 deletions(-) diff --git a/app/actions.py b/app/actions.py index b03c3093..e78b2b22 100644 --- a/app/actions.py +++ b/app/actions.py @@ -836,7 +836,7 @@ def restoreUserHistory(self): None. """ # Restore the file history. - self.fileHistory = app.history.getFileHistory(self.fullPath, self.data) + self.fileHistory = app.history.getFileHistory(self.fileStats, self.data) # Restore all positions and values of variables. self.view.cursorRow, self.view.cursorCol = self.fileHistory.setdefault( @@ -859,7 +859,7 @@ def restoreUserHistory(self): # Store the file's info. self.lastChecksum, self.lastFileSize = app.history.getFileInfo( - self.fullPath) + self.fileStats) def updateBasicScrollPosition(self): """ @@ -982,11 +982,11 @@ def fileWrite(self): if app.prefs.editor['saveUndo']: self.fileHistory['redoChainCompound'] = self.redoChain self.fileHistory['savedAtRedoIndexCompound'] = self.savedAtRedoIndex - app.history.saveUserHistory((self.fullPath, self.lastChecksum, - self.lastFileSize), self.fileHistory) + app.history.saveUserHistory((self.lastChecksum, self.lastFileSize), + self.fileStats, self.fileHistory) # Store the file's new info self.lastChecksum, self.lastFileSize = app.history.getFileInfo( - self.fullPath) + self.fileStats) self.setMessage('File saved') except Exception as e: color = app.color.get('status_line_error') diff --git a/app/buffer_manager.py b/app/buffer_manager.py index 80803c9f..b18facc2 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -134,6 +134,8 @@ def renameBuffer(self, fileBuffer, fullPath): # TODO(dschuyler): this can be phased out. It was from a time when the # buffer manager needed to know if a path changed. fileBuffer.fullPath = fullPath + # Track this file + fileBuffer.fileStats = app.file_stats.FileStats(fullPath, pollingInterval=2) def fileClose(self, path): pass diff --git a/app/file_stats.py b/app/file_stats.py index 9aee953b..5ff75e7c 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -3,7 +3,13 @@ import threading class FileStats: - def __init__(self, fullPath, pollingInterval=1): + """ + An object to monitor the statistical information of a file. To prevent + synchronization issues, If you want to retrieve multiple attributes + consecutively, you must acquire the FileStats object lock before accessing the + object's stats and release it when you are are done. + """ + def __init__(self, fullPath='', pollingInterval=2): """ Args: fullPath (str): The absolute path of the file you want to keep track of. @@ -13,21 +19,83 @@ def __init__(self, fullPath, pollingInterval=1): self.fullPath = fullPath self.fileStats = None self.lock = threading.Lock() - self.isReadOnly = True - thread = threading.Thread(target=self.run) + self.isReadOnly = False + self.threadShouldExit = False + self.thread = self.startTracking() + self.updateStats() def run(self): + while not self.threadShouldExit: + self.updateStats() + time.sleep(self.interval) + + def monitorFile(self, fullPath): + """ + Stops tracking whatever file this object was monitoring before and tracks + the newly specified file. + + Args: + None. + + Returns: + None. + """ + if self.thread: + self.threadShouldExit = True + self.thread.join() + self.threadShouldExit = False + self.fullPath = fullPath + self.thread = self.startTracking() + self.updateStats() + + def startTracking(self): + """ + Starts tracking the file whose path is specified in self.fullPath + + Args: + None. + + Returns: + The thread that was created to do the tracking. + """ + if self.fullPath: + thread = threading.Thread(target=self.run) + thread.daemon = True # Do not continue running if main program exits. + thread.start() + return thread + + def updateStats(self): + """ + Update the stats of the file in memory with the stats of the file on disk. + + Args: + None. + + Returns: + True if the file stats were updated. False if an exception occurred and + the file stats could not be updated. + """ try: - while True: - self.lock.acquire() - self.fileStats = os.stat(self.fullPath) - self.isReadOnly = os.access(self.fullPath, os.W_OK) - self.lock.release() - time.sleep(self.interval) + self.lock.acquire() + self.fileStats = os.stat(self.fullPath) + self.isReadOnly = os.access(self.fullPath, os.W_OK) + self.lock.release() + return True except Exception as e: - app.log.info("Exception occurred while running file stats thread:", e) + app.log.info("Exception occurred while updating file stats thread:", e) + self.lock.release() + return False def getFileSize(self): + """ + Calculates the size of the monitored file. + + Args: + None. + + Returns: + The size of the file in bytes. + """ if self.fileStats: return self.fileStats.st_size else: @@ -36,6 +104,9 @@ def getFileSize(self): def getFileStats(self): return self.fileStats + def getFullPath(self): + return self.fullPath + def fileIsReadOnly(self): return self.isReadOnly diff --git a/app/history.py b/app/history.py index 4baacab4..ac414dc0 100644 --- a/app/history.py +++ b/app/history.py @@ -44,13 +44,15 @@ def loadUserHistory(filePath, historyPath=pathToHistory): with open(historyPath, 'rb') as file: userHistory = pickle.load(file) -def saveUserHistory(fileInfo, fileHistory, historyPath=pathToHistory): +def saveUserHistory(fileInfo, fileStats, + fileHistory, historyPath=pathToHistory): """ Saves the user's file history by writing to a pickle file. Args: - fileInfo (tuple): Contains (filePath, lastChecksum, lastFileSize). + fileInfo (tuple): Contains (lastChecksum, lastFileSize). fileHistory (dict): The history of the file that the user wants to save. + fileStats (FileStats): The FileStat object of the file to be saved. historyPath (str): Defaults to pathToHistory. The path to the user's saved history. @@ -58,12 +60,12 @@ def saveUserHistory(fileInfo, fileHistory, historyPath=pathToHistory): None. """ global userHistory, pathToHistory - filePath, lastChecksum, lastFileSize = fileInfo + lastChecksum, lastFileSize = fileInfo try: if historyPath is not None: pathToHistory = historyPath userHistory.pop((lastChecksum, lastFileSize), None) - newChecksum, newFileSize = getFileInfo(filePath) + newChecksum, newFileSize = getFileInfo(fileStats.getFullPath(), fileStats) userHistory[(newChecksum, newFileSize)] = fileHistory with open(historyPath, 'wb') as file: pickle.dump(userHistory, file) @@ -71,7 +73,24 @@ def saveUserHistory(fileInfo, fileHistory, historyPath=pathToHistory): except Exception as e: app.log.exception(e) -def getFileHistory(filePath, data=None): +def getFileInfo(fileStats, data=None): + """ + Args: + filePath (str): The absolute path to the file. + data (str): Defaults to None. This is the data + returned by calling read() on a file object. + + Returns: + A tuple containing the checksum and size of the file. + """ + try: + checksum = calculateChecksum(fileStats.getFullPath(), data) + fileSize = fileStats.getFileSize() + return (checksum, fileSize) + except: + return (None, 0) + +def getFileHistory(fileStats, data=None): """ Takes in an file path and an optimal data argument and checks for the current file's history. @@ -81,36 +100,18 @@ def getFileHistory(filePath, data=None): so that you do not have to read the file again. Args: - filePath (str): The absolute path to the file. + fileStats (FileStats): The FileStat object of the requested file. data (str): Defaults to None. This is the data returned by calling read() on a file object. Returns: The file history (dict) of the desired file if it exists. """ - checksum, fileSize = getFileInfo(filePath, data) + checksum, fileSize = getFileInfo(fileStats, data) fileHistory = userHistory.get((checksum, fileSize), {}) fileHistory['adate'] = time.time() return fileHistory -def getFileInfo(filePath, data=None): - """ - Returns the hash value and size of the specified file. - The second argument can be passed in if a file's data has - already been read so that you do not have to read the file again. - - Args: - filePath (str): The absolute path to the file. - data (str): Defaults to None. This is the data - returned by calling read() on a file object. - - Returns: - A tuple containing the checksum and size of the file. - """ - checksum = calculateChecksum(filePath, data) - fileSize = calculateFileSize(filePath) - return (checksum, fileSize) - def calculateChecksum(filePath, data=None): """ Calculates the hash value of the specified file. @@ -137,21 +138,6 @@ def calculateChecksum(filePath, data=None): except: return None -def calculateFileSize(filePath): - """ - Calculates the size of the specified value. - - Args: - filePath (str): The absolute path to the file. - - Returns: - The size of the file in bytes. - """ - try: - return os.stat(filePath).st_size - except: - return 0 - def clearUserHistory(): """ Clears user history for all files. diff --git a/app/mutator.py b/app/mutator.py index e9c11ad4..9bcaa4d2 100644 --- a/app/mutator.py +++ b/app/mutator.py @@ -45,7 +45,7 @@ def __init__(self): self.findBackRe = None self.fileExtension = '' self.fullPath = '' - self.fileStats = app.file_stats.FileStats(self.fullPath, pollingInterval=2) + self.fileStats = None self.goalCol = 0 self.penGrammar = None self.parser = None From 994a3e31f664e3b46cf3822680b630bd6dd5db69 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 14 Dec 2017 11:19:31 -0800 Subject: [PATCH 04/39] Fixed calls to start monitoring the correct file one at a time --- app/buffer_manager.py | 2 +- app/file_stats.py | 7 ++++--- app/mutator.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/buffer_manager.py b/app/buffer_manager.py index b18facc2..60a1423f 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -135,7 +135,7 @@ def renameBuffer(self, fileBuffer, fullPath): # buffer manager needed to know if a path changed. fileBuffer.fullPath = fullPath # Track this file - fileBuffer.fileStats = app.file_stats.FileStats(fullPath, pollingInterval=2) + fileBuffer.fileStats.changeMonitoredFile(fullPath) def fileClose(self, path): pass diff --git a/app/file_stats.py b/app/file_stats.py index 5ff75e7c..24aae222 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -1,6 +1,7 @@ import os import time import threading +import app.log class FileStats: """ @@ -27,9 +28,9 @@ def __init__(self, fullPath='', pollingInterval=2): def run(self): while not self.threadShouldExit: self.updateStats() - time.sleep(self.interval) + time.sleep(self.pollingInterval) - def monitorFile(self, fullPath): + def changeMonitoredFile(self, fullPath): """ Stops tracking whatever file this object was monitoring before and tracks the newly specified file. @@ -78,7 +79,7 @@ def updateStats(self): try: self.lock.acquire() self.fileStats = os.stat(self.fullPath) - self.isReadOnly = os.access(self.fullPath, os.W_OK) + self.isReadOnly = not os.access(self.fullPath, os.W_OK) self.lock.release() return True except Exception as e: diff --git a/app/mutator.py b/app/mutator.py index 9bcaa4d2..4e643e05 100644 --- a/app/mutator.py +++ b/app/mutator.py @@ -45,7 +45,7 @@ def __init__(self): self.findBackRe = None self.fileExtension = '' self.fullPath = '' - self.fileStats = None + self.fileStats = app.file_stats.FileStats(self.fullPath) self.goalCol = 0 self.penGrammar = None self.parser = None From 680ff5c76e31464e7ec4bb3ea0e772435d2b3873 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Sat, 16 Dec 2017 16:37:36 -0800 Subject: [PATCH 05/39] Gave the fileStats object reference to the text buffer so that it can refresh the TopInfo window, but doesn't seem to be displaying in the program --- app/buffer_manager.py | 4 +++- app/file_stats.py | 19 +++++++++++++++---- app/mutator.py | 1 - app/text_buffer.py | 2 +- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/buffer_manager.py b/app/buffer_manager.py index 60a1423f..5b331e17 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -91,8 +91,10 @@ def loadTextBuffer(self, relPath, view): if not os.path.isfile(fullPath): app.log.info('creating a new file at\n ', fullPath) textBuffer = app.text_buffer.TextBuffer() - self.renameBuffer(textBuffer, fullPath) textBuffer.view = view + view.textBuffer = textBuffer + textBuffer.fileStats.setTextBuffer(textBuffer) + self.renameBuffer(textBuffer, fullPath) textBuffer.fileLoad() self.buffers.append(textBuffer) if 0: diff --git a/app/file_stats.py b/app/file_stats.py index 24aae222..351be728 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -14,11 +14,12 @@ def __init__(self, fullPath='', pollingInterval=2): """ Args: fullPath (str): The absolute path of the file you want to keep track of. - pollingInterval (int): The frequency at which you want to poll the file. + pollingInterval (float): The frequency at which you want to poll the file. """ self.pollingInterval = pollingInterval self.fullPath = fullPath self.fileStats = None + self.textBuffer = None self.lock = threading.Lock() self.isReadOnly = False self.threadShouldExit = False @@ -27,13 +28,20 @@ def __init__(self, fullPath='', pollingInterval=2): def run(self): while not self.threadShouldExit: - self.updateStats() + oldReadOnly = self.isReadOnly + if (self.updateStats() and + self.isReadOnly != oldReadOnly and + self.textBuffer and + self.textBuffer.view.textBuffer): + # This call requires the view's textbuffer to be set. + self.textBuffer.view.topInfo.onChange() time.sleep(self.pollingInterval) def changeMonitoredFile(self, fullPath): """ Stops tracking whatever file this object was monitoring before and tracks - the newly specified file. + the newly specified file. The text buffer should be set in order for the + created thread to work properly. Args: None. @@ -139,4 +147,7 @@ def fileChanged(self): s1.st_size == s2.st_size and s1.st_mtime == s2.st_mtime and s1.st_ctime == s2.st_ctime): - return s2 \ No newline at end of file + return s2 + + def setTextBuffer(self, textBuffer): + self.textBuffer = textBuffer \ No newline at end of file diff --git a/app/mutator.py b/app/mutator.py index 4e643e05..8427b722 100644 --- a/app/mutator.py +++ b/app/mutator.py @@ -45,7 +45,6 @@ def __init__(self): self.findBackRe = None self.fileExtension = '' self.fullPath = '' - self.fileStats = app.file_stats.FileStats(self.fullPath) self.goalCol = 0 self.penGrammar = None self.parser = None diff --git a/app/text_buffer.py b/app/text_buffer.py index 2d1cd55c..57018364 100644 --- a/app/text_buffer.py +++ b/app/text_buffer.py @@ -32,10 +32,10 @@ def __init__(self): self.highlightRe = None self.highlightTrailingWhitespace = True self.fileHistory = {} + self.fileStats = app.file_stats.FileStats(self.fullPath) self.fileEncoding = None self.lastChecksum = None self.lastFileSize = 0 - self.fileStats = app.file_stats.FileStats(self.fullPath, pollingInterval=2) self.bookmarks = [] self.nextBookmarkColorPos = 0 From 13514cc4f34c5c5059cd2a1b07111fe7ce1a9f8c Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Wed, 20 Dec 2017 09:01:08 -0800 Subject: [PATCH 06/39] Changed from rendering the topInfo window to rerendering the entire window --- app/buffer_manager.py | 6 +----- app/file_stats.py | 2 +- app/window.py | 3 +++ 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/buffer_manager.py b/app/buffer_manager.py index 5b331e17..80803c9f 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -91,10 +91,8 @@ def loadTextBuffer(self, relPath, view): if not os.path.isfile(fullPath): app.log.info('creating a new file at\n ', fullPath) textBuffer = app.text_buffer.TextBuffer() - textBuffer.view = view - view.textBuffer = textBuffer - textBuffer.fileStats.setTextBuffer(textBuffer) self.renameBuffer(textBuffer, fullPath) + textBuffer.view = view textBuffer.fileLoad() self.buffers.append(textBuffer) if 0: @@ -136,8 +134,6 @@ def renameBuffer(self, fileBuffer, fullPath): # TODO(dschuyler): this can be phased out. It was from a time when the # buffer manager needed to know if a path changed. fileBuffer.fullPath = fullPath - # Track this file - fileBuffer.fileStats.changeMonitoredFile(fullPath) def fileClose(self, path): pass diff --git a/app/file_stats.py b/app/file_stats.py index 351be728..fe8bdfb8 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -34,7 +34,7 @@ def run(self): self.textBuffer and self.textBuffer.view.textBuffer): # This call requires the view's textbuffer to be set. - self.textBuffer.view.topInfo.onChange() + self.textBuffer.view.render() time.sleep(self.pollingInterval) def changeMonitoredFile(self, fullPath): diff --git a/app/window.py b/app/window.py index e5865479..d76baa83 100755 --- a/app/window.py +++ b/app/window.py @@ -771,6 +771,9 @@ def startup(self): if not tb: tb = app.buffer_manager.buffers.newTextBuffer() self.setTextBuffer(tb) + # Make this text buffer track the file its in charge of. + tb.fileStats.setTextBuffer(tb) + tb.fileStats.changeMonitoredFile(tb.fullPath) openToLine = app.prefs.startup.get('openToLine') if openToLine is not None: self.textBuffer.selectText(openToLine - 1, 0, 0, From 5141adc28c230b513965fdcbbff5a0b2a20cb14e Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 08:51:07 -0800 Subject: [PATCH 07/39] Changed implementation to make the filestats object talk to the background thread --- app/background.py | 8 +++++++- app/ci_program.py | 6 +++--- app/file_stats.py | 26 ++++++++++++++++---------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/app/background.py b/app/background.py index 22d70e9d..4a67240d 100644 --- a/app/background.py +++ b/app/background.py @@ -52,11 +52,17 @@ def background(inputQueue, outputQueue): while True: try: try: - program, message = inputQueue.get(block) + program, message, callerSema = inputQueue.get(block) #profile = app.profile.beginPythonProfile() if message == 'quit': app.log.info('bg received quit message') return + elif message == 'refresh': + app.log.info('bg received refresh message') + assert(type(callerSema) == threading.Semaphore) + program.render() + callerSema.release() + return program.executeCommandList(message) program.focusedWindow.textBuffer.parseScreenMaybe() program.render() diff --git a/app/ci_program.py b/app/ci_program.py index 2e12c1bc..6bae3ad1 100755 --- a/app/ci_program.py +++ b/app/ci_program.py @@ -115,7 +115,7 @@ def commandLoop(self): start = time.time() # The first render, to get something on the screen. if useBgThread: - self.bg.put((self, [])) + self.bg.put((self, [], None)) else: self.render() # This is the 'main loop'. Execution doesn't leave this loop until the @@ -232,7 +232,7 @@ def commandLoop(self): start = time.time() if len(cmdList): if useBgThread: - self.bg.put((self, cmdList)) + self.bg.put((self, cmdList, None)) else: self.executeCommandList(cmdList) self.render() @@ -696,7 +696,7 @@ def run(self): else: self.commandLoop() if app.prefs.editor['useBgThread']: - self.bg.put((self, 'quit')) + self.bg.put((self, 'quit', None)) self.bg.join() def setUpPalette(self): diff --git a/app/file_stats.py b/app/file_stats.py index fe8bdfb8..119a0f4b 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -1,7 +1,8 @@ +import app.background +import app.log import os import time import threading -import app.log class FileStats: """ @@ -16,12 +17,13 @@ def __init__(self, fullPath='', pollingInterval=2): fullPath (str): The absolute path of the file you want to keep track of. pollingInterval (float): The frequency at which you want to poll the file. """ - self.pollingInterval = pollingInterval self.fullPath = fullPath self.fileStats = None - self.textBuffer = None - self.lock = threading.Lock() self.isReadOnly = False + self.pollingInterval = pollingInterval + self.threadSema = threading.Semaphore(0) + self.statsLock = threading.Lock() + self.textBuffer = None self.threadShouldExit = False self.thread = self.startTracking() self.updateStats() @@ -33,8 +35,9 @@ def run(self): self.isReadOnly != oldReadOnly and self.textBuffer and self.textBuffer.view.textBuffer): - # This call requires the view's textbuffer to be set. - self.textBuffer.view.render() + self.threadSema.acquire() + app.background.bg.put( + (self.textBuffer.view.host, 'refresh', self.threadSema)) time.sleep(self.pollingInterval) def changeMonitoredFile(self, fullPath): @@ -54,8 +57,8 @@ def changeMonitoredFile(self, fullPath): self.thread.join() self.threadShouldExit = False self.fullPath = fullPath - self.thread = self.startTracking() self.updateStats() + self.thread = self.startTracking() def startTracking(self): """ @@ -85,14 +88,14 @@ def updateStats(self): the file stats could not be updated. """ try: - self.lock.acquire() + self.statsLock.acquire() self.fileStats = os.stat(self.fullPath) self.isReadOnly = not os.access(self.fullPath, os.W_OK) - self.lock.release() + self.statsLock.release() return True except Exception as e: app.log.info("Exception occurred while updating file stats thread:", e) - self.lock.release() + self.statsLock.release() return False def getFileSize(self): @@ -110,12 +113,15 @@ def getFileSize(self): else: return 0 + # Not thread safe. Must acquire self.statsLock() before calling this function. def getFileStats(self): return self.fileStats + # Not thread safe. Must acquire self.statsLock() before calling this function. def getFullPath(self): return self.fullPath + # Not thread safe. Must acquire self.statsLock() before calling this function. def fileIsReadOnly(self): return self.isReadOnly From d78721f7e6040df2d80f2864bf091c491a2ecd5b Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 09:36:00 -0800 Subject: [PATCH 08/39] Fixed program from crashing due to returning from bg thread early --- app/background.py | 4 ++-- app/file_stats.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/background.py b/app/background.py index 4a67240d..58124c44 100644 --- a/app/background.py +++ b/app/background.py @@ -59,10 +59,10 @@ def background(inputQueue, outputQueue): return elif message == 'refresh': app.log.info('bg received refresh message') - assert(type(callerSema) == threading.Semaphore) + assert(callerSema != None) program.render() callerSema.release() - return + continue program.executeCommandList(message) program.focusedWindow.textBuffer.parseScreenMaybe() program.render() diff --git a/app/file_stats.py b/app/file_stats.py index 119a0f4b..a967da30 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -3,6 +3,7 @@ import os import time import threading +import app.window class FileStats: """ @@ -35,9 +36,10 @@ def run(self): self.isReadOnly != oldReadOnly and self.textBuffer and self.textBuffer.view.textBuffer): - self.threadSema.acquire() + app.log.meta("putting on bg") app.background.bg.put( (self.textBuffer.view.host, 'refresh', self.threadSema)) + self.threadSema.acquire() time.sleep(self.pollingInterval) def changeMonitoredFile(self, fullPath): From ba39a45092a4c2d76e7deeef82c8d19bcef5f63b Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 10:15:17 -0800 Subject: [PATCH 09/39] Made filestats thread work correctly and refactored the background function in background.py. Also renamed refresh to redraw --- app/background.py | 35 ++++++++++++++++++++++------------- app/file_stats.py | 3 +-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/app/background.py b/app/background.py index 58124c44..10bafcce 100644 --- a/app/background.py +++ b/app/background.py @@ -46,9 +46,23 @@ def put(self, data): def background(inputQueue, outputQueue): + def redrawProgram(program): + """ + Sends a SIGUSR1 signal to the current program and draws its screen. + + Args: + program (CiProgram): an instance of the CiProgram object. + + Returns: + None. + """ + pid = os.getpid() + signalNumber = signal.SIGUSR1 + program.render() + outputQueue.put(app.render.frame.grabFrame()) + os.kill(pid, signalNumber) + block = True - pid = os.getpid() - signalNumber = signal.SIGUSR1 while True: try: try: @@ -57,17 +71,14 @@ def background(inputQueue, outputQueue): if message == 'quit': app.log.info('bg received quit message') return - elif message == 'refresh': - app.log.info('bg received refresh message') + elif message == 'redraw': + app.log.info('bg received redraw message') assert(callerSema != None) - program.render() + redrawProgram(program) callerSema.release() continue program.executeCommandList(message) - program.focusedWindow.textBuffer.parseScreenMaybe() - program.render() - outputQueue.put(app.render.frame.grabFrame()) - os.kill(pid, signalNumber) + redrawProgram(program) #app.profile.endPythonProfile(profile) if not inputQueue.empty(): continue @@ -80,16 +91,14 @@ def background(inputQueue, outputQueue): program.focusedWindow.textBuffer.parseDocument() block = len(tb.parser.rows) >= len(tb.lines) if block: - program.render() - outputQueue.put(app.render.frame.grabFrame()) - os.kill(pid, signalNumber) + redrawProgram(program) except Exception as e: app.log.exception(e) app.log.error('bg thread exception', e) errorType, value, tracebackInfo = sys.exc_info() out = traceback.format_exception(errorType, value, tracebackInfo) outputQueue.put(('exception', out)) - os.kill(pid, signalNumber) + os.kill(os.getpid(), signal.SIGUSR1) while True: program, message = inputQueue.get() if message == 'quit': diff --git a/app/file_stats.py b/app/file_stats.py index a967da30..3f54ba41 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -36,9 +36,8 @@ def run(self): self.isReadOnly != oldReadOnly and self.textBuffer and self.textBuffer.view.textBuffer): - app.log.meta("putting on bg") app.background.bg.put( - (self.textBuffer.view.host, 'refresh', self.threadSema)) + (self.textBuffer.view.host, 'redraw', self.threadSema)) self.threadSema.acquire() time.sleep(self.pollingInterval) From 1ec739eb40216e0671371d39b5e46dd81f90ba9d Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 11:03:40 -0800 Subject: [PATCH 10/39] Made only one function in fileStats that returns all the data that the program needs, which will prevent synchronization issues from occurring. Saving redo chain has now broken and needs to be fixed and still need to support fileStats when in singleThread mode --- app/actions.py | 2 +- app/buffer_manager.py | 4 ++++ app/file_stats.py | 48 ++++++++++++++----------------------------- app/history.py | 17 ++++++++------- app/window.py | 2 +- 5 files changed, 29 insertions(+), 44 deletions(-) diff --git a/app/actions.py b/app/actions.py index e78b2b22..26bc0d00 100644 --- a/app/actions.py +++ b/app/actions.py @@ -990,7 +990,7 @@ def fileWrite(self): self.setMessage('File saved') except Exception as e: color = app.color.get('status_line_error') - if self.fileStats.fileIsReadOnly(): + if self.fileStats.getTrackedFileInfo()['isReadOnly']: self.setMessage("Permission error. Try modifing in sudo mode.", color=color) else: diff --git a/app/buffer_manager.py b/app/buffer_manager.py index 80803c9f..0b19e68b 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -131,6 +131,10 @@ def untrackBuffer_(self, fileBuffer): self.buffers.remove(fileBuffer) def renameBuffer(self, fileBuffer, fullPath): + """ + For now, when you change the path of a fileBuffer, you should also be + updating its fileStat object, so that it tracks the new file as well. + """ # TODO(dschuyler): this can be phased out. It was from a time when the # buffer manager needed to know if a path changed. fileBuffer.fullPath = fullPath diff --git a/app/file_stats.py b/app/file_stats.py index 3f54ba41..bb7b5adc 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -19,9 +19,11 @@ def __init__(self, fullPath='', pollingInterval=2): pollingInterval (float): The frequency at which you want to poll the file. """ self.fullPath = fullPath - self.fileStats = None - self.isReadOnly = False + self.__fileStats = None self.pollingInterval = pollingInterval + # All necessary file info should be placed in this dictionary. + self.fileInfo = {'isReadOnly': False, + 'size': 0} self.threadSema = threading.Semaphore(0) self.statsLock = threading.Lock() self.textBuffer = None @@ -31,9 +33,9 @@ def __init__(self, fullPath='', pollingInterval=2): def run(self): while not self.threadShouldExit: - oldReadOnly = self.isReadOnly + oldFileIsReadOnly = self.getTrackedFileInfo()['isReadOnly'] if (self.updateStats() and - self.isReadOnly != oldReadOnly and + self.getTrackedFileInfo()['isReadOnly'] != oldFileIsReadOnly and self.textBuffer and self.textBuffer.view.textBuffer): app.background.bg.put( @@ -90,8 +92,9 @@ def updateStats(self): """ try: self.statsLock.acquire() - self.fileStats = os.stat(self.fullPath) - self.isReadOnly = not os.access(self.fullPath, os.W_OK) + self.__fileStats = os.stat(self.fullPath) + self.fileInfo['isReadOnly'] = not os.access(self.fullPath, os.W_OK) + self.fileInfo['size'] = self.__fileStats.st_size self.statsLock.release() return True except Exception as e: @@ -99,32 +102,11 @@ def updateStats(self): self.statsLock.release() return False - def getFileSize(self): - """ - Calculates the size of the monitored file. - - Args: - None. - - Returns: - The size of the file in bytes. - """ - if self.fileStats: - return self.fileStats.st_size - else: - return 0 - - # Not thread safe. Must acquire self.statsLock() before calling this function. - def getFileStats(self): - return self.fileStats - - # Not thread safe. Must acquire self.statsLock() before calling this function. - def getFullPath(self): - return self.fullPath - - # Not thread safe. Must acquire self.statsLock() before calling this function. - def fileIsReadOnly(self): - return self.isReadOnly + def getTrackedFileInfo(self): + self.statsLock.acquire() + info = self.fileInfo + self.statsLock.release() + return info def fileChanged(self): """ @@ -136,7 +118,7 @@ def fileChanged(self): Returns: The new file stats if the file has changed. Otherwise, None. """ - s1 = self.fileStats + s1 = self.__fileStats s2 = os.stat(self.fullPath) app.log.info('st_mode', s1.st_mode, s2.st_mode) app.log.info('st_ino', s1.st_ino, s2.st_ino) diff --git a/app/history.py b/app/history.py index ac414dc0..86b3a4d0 100644 --- a/app/history.py +++ b/app/history.py @@ -59,13 +59,14 @@ def saveUserHistory(fileInfo, fileStats, Returns: None. """ + import pdb; pdb.set_trace() global userHistory, pathToHistory lastChecksum, lastFileSize = fileInfo try: if historyPath is not None: pathToHistory = historyPath userHistory.pop((lastChecksum, lastFileSize), None) - newChecksum, newFileSize = getFileInfo(fileStats.getFullPath(), fileStats) + newChecksum, newFileSize = getFileInfo(fileStats) userHistory[(newChecksum, newFileSize)] = fileHistory with open(historyPath, 'wb') as file: pickle.dump(userHistory, file) @@ -76,19 +77,17 @@ def saveUserHistory(fileInfo, fileStats, def getFileInfo(fileStats, data=None): """ Args: - filePath (str): The absolute path to the file. + fileStats (FileStats): a FileStats object of a file. data (str): Defaults to None. This is the data returned by calling read() on a file object. Returns: - A tuple containing the checksum and size of the file. + A tuple containing the (checksum, fileSize) of the file. """ - try: - checksum = calculateChecksum(fileStats.getFullPath(), data) - fileSize = fileStats.getFileSize() - return (checksum, fileSize) - except: - return (None, 0) + fileInfo = fileStats.getTrackedFileInfo() + checksum = calculateChecksum(fileStats.fullPath, data) + fileSize = fileInfo['size'] + return (checksum, fileSize) def getFileHistory(fileStats, data=None): """ diff --git a/app/window.py b/app/window.py index d76baa83..697fdca1 100755 --- a/app/window.py +++ b/app/window.py @@ -627,7 +627,7 @@ def onChange(self): lineCursor -= 1 pathLine = self.host.textBuffer.fullPath if 1: - if tb.fileStats.fileIsReadOnly(): + if tb.fileStats.getTrackedFileInfo()['isReadOnly']: pathLine += ' [RO]' if 1: if tb.isDirty(): From b3295d48d27ce3b6dfb30989f68ff7030872dd5f Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 11:30:13 -0800 Subject: [PATCH 11/39] Implemented single-threaded version for fileStats and fixed redo chain for single-threaded version --- app/buffer_manager.py | 1 - app/file_stats.py | 57 ++++++++++++++++--------------------------- app/history.py | 1 - app/window.py | 1 + 4 files changed, 22 insertions(+), 38 deletions(-) diff --git a/app/buffer_manager.py b/app/buffer_manager.py index 0b19e68b..7c092af9 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -93,7 +93,6 @@ def loadTextBuffer(self, relPath, view): textBuffer = app.text_buffer.TextBuffer() self.renameBuffer(textBuffer, fullPath) textBuffer.view = view - textBuffer.fileLoad() self.buffers.append(textBuffer) if 0: self.debugLog() diff --git a/app/file_stats.py b/app/file_stats.py index bb7b5adc..a78d4dc5 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -1,5 +1,6 @@ import app.background import app.log +import app.prefs import os import time import threading @@ -24,11 +25,16 @@ def __init__(self, fullPath='', pollingInterval=2): # All necessary file info should be placed in this dictionary. self.fileInfo = {'isReadOnly': False, 'size': 0} - self.threadSema = threading.Semaphore(0) self.statsLock = threading.Lock() self.textBuffer = None - self.threadShouldExit = False - self.thread = self.startTracking() + if app.prefs.editor['useBgThread']: + self.threadSema = threading.Semaphore(0) + self.threadShouldExit = False + self.thread = self.startTracking() + else: + self.threadSema = None + self.threadShouldExit = True + self.thread = None self.updateStats() def run(self): @@ -73,7 +79,7 @@ def startTracking(self): Returns: The thread that was created to do the tracking. """ - if self.fullPath: + if self.fullPath and app.prefs.editor['useBgThread']: thread = threading.Thread(target=self.run) thread.daemon = True # Do not continue running if main program exits. thread.start() @@ -103,40 +109,19 @@ def updateStats(self): return False def getTrackedFileInfo(self): - self.statsLock.acquire() - info = self.fileInfo - self.statsLock.release() - return info - - def fileChanged(self): """ - Compares the file's stats with the recorded stats we have in memory. - - Args: - None. - - Returns: - The new file stats if the file has changed. Otherwise, None. + Returns the most recent information about the tracked file. If running in + singleThread mode, this will sync the in-memory file info with the + file info that is on disk and then return the updated file info. """ - s1 = self.__fileStats - s2 = os.stat(self.fullPath) - app.log.info('st_mode', s1.st_mode, s2.st_mode) - app.log.info('st_ino', s1.st_ino, s2.st_ino) - app.log.info('st_dev', s1.st_dev, s2.st_dev) - app.log.info('st_uid', s1.st_uid, s2.st_uid) - app.log.info('st_gid', s1.st_gid, s2.st_gid) - app.log.info('st_size', s1.st_size, s2.st_size) - app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) - app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) - if (s1.st_mode == s2.st_mode and - s1.st_ino == s2.st_ino and - s1.st_dev == s2.st_dev and - s1.st_uid == s2.st_uid and - s1.st_gid == s2.st_gid and - s1.st_size == s2.st_size and - s1.st_mtime == s2.st_mtime and - s1.st_ctime == s2.st_ctime): - return s2 + if app.prefs.editor['useBgThread']: + self.statsLock.acquire() + info = self.fileInfo + self.statsLock.release() + else: + self.updateStats() + info = self.fileInfo + return info def setTextBuffer(self, textBuffer): self.textBuffer = textBuffer \ No newline at end of file diff --git a/app/history.py b/app/history.py index 86b3a4d0..0dd0c818 100644 --- a/app/history.py +++ b/app/history.py @@ -59,7 +59,6 @@ def saveUserHistory(fileInfo, fileStats, Returns: None. """ - import pdb; pdb.set_trace() global userHistory, pathToHistory lastChecksum, lastFileSize = fileInfo try: diff --git a/app/window.py b/app/window.py index 697fdca1..1a99a6bd 100755 --- a/app/window.py +++ b/app/window.py @@ -774,6 +774,7 @@ def startup(self): # Make this text buffer track the file its in charge of. tb.fileStats.setTextBuffer(tb) tb.fileStats.changeMonitoredFile(tb.fullPath) + tb.fileLoad() openToLine = app.prefs.startup.get('openToLine') if openToLine is not None: self.textBuffer.selectText(openToLine - 1, 0, 0, From d4df6dc79ed509eff1dc21f49c47c8b19451258f Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 17:26:28 -0800 Subject: [PATCH 12/39] Fixed synch issue which also fixed the fileStats thread when running in multithreaded mode. Also renamed getTrackedFileInfo to getUpdatedFileInfo to better reflect its functionality --- app/actions.py | 2 +- app/file_stats.py | 28 ++++++++++++---------------- app/history.py | 2 +- app/window.py | 2 +- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/app/actions.py b/app/actions.py index 26bc0d00..94e124ac 100644 --- a/app/actions.py +++ b/app/actions.py @@ -990,7 +990,7 @@ def fileWrite(self): self.setMessage('File saved') except Exception as e: color = app.color.get('status_line_error') - if self.fileStats.getTrackedFileInfo()['isReadOnly']: + if self.fileStats.getUpdatedFileInfo()['isReadOnly']: self.setMessage("Permission error. Try modifing in sudo mode.", color=color) else: diff --git a/app/file_stats.py b/app/file_stats.py index a78d4dc5..a1201f43 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -39,11 +39,11 @@ def __init__(self, fullPath='', pollingInterval=2): def run(self): while not self.threadShouldExit: - oldFileIsReadOnly = self.getTrackedFileInfo()['isReadOnly'] - if (self.updateStats() and - self.getTrackedFileInfo()['isReadOnly'] != oldFileIsReadOnly and - self.textBuffer and - self.textBuffer.view.textBuffer): + # Redraw the screen if the file changed READ ONLY permissions. + oldFileIsReadOnly = self.fileInfo['isReadOnly'] + newFileIsReadOnly = self.getUpdatedFileInfo()['isReadOnly'] + if (newFileIsReadOnly != oldFileIsReadOnly and + self.textBuffer and self.textBuffer.view.textBuffer): app.background.bg.put( (self.textBuffer.view.host, 'redraw', self.threadSema)) self.threadSema.acquire() @@ -108,19 +108,15 @@ def updateStats(self): self.statsLock.release() return False - def getTrackedFileInfo(self): + def getUpdatedFileInfo(self): """ - Returns the most recent information about the tracked file. If running in - singleThread mode, this will sync the in-memory file info with the - file info that is on disk and then return the updated file info. + Syncs the in-memory file information with the information on disk. It + then returns the newly updated file information. """ - if app.prefs.editor['useBgThread']: - self.statsLock.acquire() - info = self.fileInfo - self.statsLock.release() - else: - self.updateStats() - info = self.fileInfo + self.updateStats() + self.statsLock.acquire() + info = self.fileInfo.copy() # Shallow copy. + self.statsLock.release() return info def setTextBuffer(self, textBuffer): diff --git a/app/history.py b/app/history.py index 0dd0c818..b59a545c 100644 --- a/app/history.py +++ b/app/history.py @@ -83,7 +83,7 @@ def getFileInfo(fileStats, data=None): Returns: A tuple containing the (checksum, fileSize) of the file. """ - fileInfo = fileStats.getTrackedFileInfo() + fileInfo = fileStats.getUpdatedFileInfo() checksum = calculateChecksum(fileStats.fullPath, data) fileSize = fileInfo['size'] return (checksum, fileSize) diff --git a/app/window.py b/app/window.py index 1a99a6bd..c9b19c67 100755 --- a/app/window.py +++ b/app/window.py @@ -627,7 +627,7 @@ def onChange(self): lineCursor -= 1 pathLine = self.host.textBuffer.fullPath if 1: - if tb.fileStats.getTrackedFileInfo()['isReadOnly']: + if tb.fileStats.getUpdatedFileInfo()['isReadOnly']: pathLine += ' [RO]' if 1: if tb.isDirty(): From c149754f95e9f0c326b08675df32ae09b4facd18 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 17:35:27 -0800 Subject: [PATCH 13/39] Add empty line to end of file --- app/file_stats.py | 2 +- app/history.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/file_stats.py b/app/file_stats.py index a1201f43..cf6fe6b7 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -120,4 +120,4 @@ def getUpdatedFileInfo(self): return info def setTextBuffer(self, textBuffer): - self.textBuffer = textBuffer \ No newline at end of file + self.textBuffer = textBuffer diff --git a/app/history.py b/app/history.py index b59a545c..95c01b04 100644 --- a/app/history.py +++ b/app/history.py @@ -153,4 +153,3 @@ def clearUserHistory(): app.log.info("user history cleared") except Exception as e: app.log.error('clearUserHistory exception', e) - From d0eb68cf7e8641d5e99c366d0665607b38243b3c Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 17:38:11 -0800 Subject: [PATCH 14/39] Update old comments since arguments were changed after refactoring and reimplementing --- app/history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/history.py b/app/history.py index 95c01b04..62e86746 100644 --- a/app/history.py +++ b/app/history.py @@ -51,8 +51,8 @@ def saveUserHistory(fileInfo, fileStats, Args: fileInfo (tuple): Contains (lastChecksum, lastFileSize). - fileHistory (dict): The history of the file that the user wants to save. fileStats (FileStats): The FileStat object of the file to be saved. + fileHistory (dict): The history of the file that the user wants to save. historyPath (str): Defaults to pathToHistory. The path to the user's saved history. From 91ddfd1d71102c4b62de9a3356b74624bb06be20 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 18:41:48 -0800 Subject: [PATCH 15/39] Fix isSafeToWrite function to utilize the fileStats object --- app/actions.py | 2 ++ app/file_stats.py | 6 +++--- app/history.py | 4 ++++ app/mutator.py | 21 ++++++++++++++++++++- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/app/actions.py b/app/actions.py index 94e124ac..cdaebf34 100644 --- a/app/actions.py +++ b/app/actions.py @@ -805,6 +805,7 @@ def fileLoad(self): app.log.info('error opening file', self.fullPath) self.setMessage('error opening file', self.fullPath) return + self.savedFileStat = self.fileStats.fileStats self.relativePath = os.path.relpath(self.fullPath, os.getcwd()) app.log.info('fullPath', self.fullPath) app.log.info('cwd', os.getcwd()) @@ -987,6 +988,7 @@ def fileWrite(self): # Store the file's new info self.lastChecksum, self.lastFileSize = app.history.getFileInfo( self.fileStats) + self.savedFileStat = self.fileStats.fileStats self.setMessage('File saved') except Exception as e: color = app.color.get('status_line_error') diff --git a/app/file_stats.py b/app/file_stats.py index cf6fe6b7..8becb7fb 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -20,7 +20,7 @@ def __init__(self, fullPath='', pollingInterval=2): pollingInterval (float): The frequency at which you want to poll the file. """ self.fullPath = fullPath - self.__fileStats = None + self.fileStats = None self.pollingInterval = pollingInterval # All necessary file info should be placed in this dictionary. self.fileInfo = {'isReadOnly': False, @@ -98,9 +98,9 @@ def updateStats(self): """ try: self.statsLock.acquire() - self.__fileStats = os.stat(self.fullPath) + self.fileStats = os.stat(self.fullPath) self.fileInfo['isReadOnly'] = not os.access(self.fullPath, os.W_OK) - self.fileInfo['size'] = self.__fileStats.st_size + self.fileInfo['size'] = self.fileStats.st_size self.statsLock.release() return True except Exception as e: diff --git a/app/history.py b/app/history.py index 62e86746..a5e9ca48 100644 --- a/app/history.py +++ b/app/history.py @@ -75,6 +75,10 @@ def saveUserHistory(fileInfo, fileStats, def getFileInfo(fileStats, data=None): """ + Returns the hash value and size of the specified file. + The second argument can be passed in if a file's data has + already been read so that you do not have to read the file again. + Args: fileStats (FileStats): a FileStats object of a file. data (str): Defaults to None. This is the data diff --git a/app/mutator.py b/app/mutator.py index 8427b722..7720010b 100644 --- a/app/mutator.py +++ b/app/mutator.py @@ -46,6 +46,7 @@ def __init__(self): self.fileExtension = '' self.fullPath = '' self.goalCol = 0 + self.savedFileStat = None self.penGrammar = None self.parser = None self.parserTime = .0 @@ -138,7 +139,25 @@ def isDirty(self): def isSafeToWrite(self): if not os.path.exists(self.fullPath): return True - #CALL THE FILESTATS OBJECT HERE + self.fileStats.updateStats() + s1 = self.fileStats.fileStats + s2 = self.savedFileStat + app.log.info('st_mode', s1.st_mode, s2.st_mode) + app.log.info('st_ino', s1.st_ino, s2.st_ino) + app.log.info('st_dev', s1.st_dev, s2.st_dev) + app.log.info('st_uid', s1.st_uid, s2.st_uid) + app.log.info('st_gid', s1.st_gid, s2.st_gid) + app.log.info('st_size', s1.st_size, s2.st_size) + app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) + app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) + return (s1.st_mode == s2.st_mode and + s1.st_ino == s2.st_ino and + s1.st_dev == s2.st_dev and + s1.st_uid == s2.st_uid and + s1.st_gid == s2.st_gid and + s1.st_size == s2.st_size and + s1.st_mtime == s2.st_mtime and + s1.st_ctime == s2.st_ctime) def __doMoveLines(self, begin, end, to): From c744b86be8b9ebfda8568e50c959cfdf4a02b6fb Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 19:40:21 -0800 Subject: [PATCH 16/39] Fixed comments and moved fileLoad and setting fileStats attributes back to the loadTextBuffer() function --- app/buffer_manager.py | 4 ++++ app/file_stats.py | 19 +++++++++---------- app/window.py | 4 ---- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/buffer_manager.py b/app/buffer_manager.py index 7c092af9..1ca2143d 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -93,6 +93,10 @@ def loadTextBuffer(self, relPath, view): textBuffer = app.text_buffer.TextBuffer() self.renameBuffer(textBuffer, fullPath) textBuffer.view = view + # Make this text buffer track the file its in charge of. + textBuffer.fileStats.setTextBuffer(textBuffer) + textBuffer.fileStats.changeMonitoredFile(fullPath) + textBuffer.fileLoad() self.buffers.append(textBuffer) if 0: self.debugLog() diff --git a/app/file_stats.py b/app/file_stats.py index 8becb7fb..ddfbba17 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -8,10 +8,10 @@ class FileStats: """ - An object to monitor the statistical information of a file. To prevent - synchronization issues, If you want to retrieve multiple attributes - consecutively, you must acquire the FileStats object lock before accessing the - object's stats and release it when you are are done. + An object to monitor the statistical information of a file. It will + automatically update the information through polling if multithreading + is allowed. Otherwise, you must either call updateStats() or + getUpdatedFileInfo() to obtain the updated information from disk. """ def __init__(self, fullPath='', pollingInterval=2): """ @@ -42,18 +42,17 @@ def run(self): # Redraw the screen if the file changed READ ONLY permissions. oldFileIsReadOnly = self.fileInfo['isReadOnly'] newFileIsReadOnly = self.getUpdatedFileInfo()['isReadOnly'] - if (newFileIsReadOnly != oldFileIsReadOnly and - self.textBuffer and self.textBuffer.view.textBuffer): - app.background.bg.put( - (self.textBuffer.view.host, 'redraw', self.threadSema)) + program = self.textBuffer.view.host + if newFileIsReadOnly != oldFileIsReadOnly and program: + app.background.bg.put((program, 'redraw', self.threadSema)) self.threadSema.acquire() time.sleep(self.pollingInterval) def changeMonitoredFile(self, fullPath): """ Stops tracking whatever file this object was monitoring before and tracks - the newly specified file. The text buffer should be set in order for the - created thread to work properly. + the newly specified file. The self.textBuffer attribute must + be set in order for the created thread to work properly. Args: None. diff --git a/app/window.py b/app/window.py index c9b19c67..2c52632e 100755 --- a/app/window.py +++ b/app/window.py @@ -771,10 +771,6 @@ def startup(self): if not tb: tb = app.buffer_manager.buffers.newTextBuffer() self.setTextBuffer(tb) - # Make this text buffer track the file its in charge of. - tb.fileStats.setTextBuffer(tb) - tb.fileStats.changeMonitoredFile(tb.fullPath) - tb.fileLoad() openToLine = app.prefs.startup.get('openToLine') if openToLine is not None: self.textBuffer.selectText(openToLine - 1, 0, 0, From 7cdee9a3b45233c4575fdbed5e1c278980588b4c Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 21 Dec 2017 20:22:20 -0800 Subject: [PATCH 17/39] Now if renameBuffer is called, the function will also modify the fileStats object so that it will track the new file. Also created a FileTracker object that inherits from threading.Thread in order to speed up the killing of threads. --- app/background.py | 6 +++--- app/buffer_manager.py | 3 ++- app/file_stats.py | 48 +++++++++++++++++++++++-------------------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/app/background.py b/app/background.py index 10bafcce..3df188ea 100644 --- a/app/background.py +++ b/app/background.py @@ -66,16 +66,16 @@ def redrawProgram(program): while True: try: try: - program, message, callerSema = inputQueue.get(block) + program, message, callerSemaphore = inputQueue.get(block) #profile = app.profile.beginPythonProfile() if message == 'quit': app.log.info('bg received quit message') return elif message == 'redraw': app.log.info('bg received redraw message') - assert(callerSema != None) + assert(callerSemaphore != None) redrawProgram(program) - callerSema.release() + callerSemaphore.release() continue program.executeCommandList(message) redrawProgram(program) diff --git a/app/buffer_manager.py b/app/buffer_manager.py index 1ca2143d..ea5fc139 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -91,8 +91,8 @@ def loadTextBuffer(self, relPath, view): if not os.path.isfile(fullPath): app.log.info('creating a new file at\n ', fullPath) textBuffer = app.text_buffer.TextBuffer() - self.renameBuffer(textBuffer, fullPath) textBuffer.view = view + self.renameBuffer(textBuffer, fullPath) # Make this text buffer track the file its in charge of. textBuffer.fileStats.setTextBuffer(textBuffer) textBuffer.fileStats.changeMonitoredFile(fullPath) @@ -141,6 +141,7 @@ def renameBuffer(self, fileBuffer, fullPath): # TODO(dschuyler): this can be phased out. It was from a time when the # buffer manager needed to know if a path changed. fileBuffer.fullPath = fullPath + fileBuffer.fileStats.changeMonitoredFile(fullPath) def fileClose(self, path): pass diff --git a/app/file_stats.py b/app/file_stats.py index ddfbba17..1ced4005 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -6,6 +6,13 @@ import threading import app.window + +class FileTracker(threading.Thread): + def __init__(self, *args, **keywords): + threading.Thread.__init__(self, *args, **keywords) + self.shouldExit = False + self.semaphore = threading.Semaphore(0) + class FileStats: """ An object to monitor the statistical information of a file. It will @@ -13,6 +20,7 @@ class FileStats: is allowed. Otherwise, you must either call updateStats() or getUpdatedFileInfo() to obtain the updated information from disk. """ + def __init__(self, fullPath='', pollingInterval=2): """ Args: @@ -27,26 +35,23 @@ def __init__(self, fullPath='', pollingInterval=2): 'size': 0} self.statsLock = threading.Lock() self.textBuffer = None - if app.prefs.editor['useBgThread']: - self.threadSema = threading.Semaphore(0) - self.threadShouldExit = False - self.thread = self.startTracking() - else: - self.threadSema = None - self.threadShouldExit = True - self.thread = None + self.thread = self.startTracking() self.updateStats() def run(self): - while not self.threadShouldExit: + while not self.thread.shouldExit: # Redraw the screen if the file changed READ ONLY permissions. oldFileIsReadOnly = self.fileInfo['isReadOnly'] newFileIsReadOnly = self.getUpdatedFileInfo()['isReadOnly'] program = self.textBuffer.view.host + turnoverTime = 0 if newFileIsReadOnly != oldFileIsReadOnly and program: - app.background.bg.put((program, 'redraw', self.threadSema)) - self.threadSema.acquire() - time.sleep(self.pollingInterval) + before = time.time() + app.background.bg.put((program, 'redraw', self.thread.semaphore)) + # Wait for bg thread to finish refreshing before sleeping + self.thread.semaphore.acquire() + turnoverTime = time.time() - before + time.sleep(max(self.pollingInterval - turnoverTime, 0)) def changeMonitoredFile(self, fullPath): """ @@ -61,28 +66,27 @@ def changeMonitoredFile(self, fullPath): None. """ if self.thread: - self.threadShouldExit = True - self.thread.join() - self.threadShouldExit = False + self.thread.shouldExit = True self.fullPath = fullPath self.updateStats() - self.thread = self.startTracking() + self.startTracking() def startTracking(self): """ - Starts tracking the file whose path is specified in self.fullPath + Starts tracking the file whose path is specified in self.fullPath. Sets + self.thread to this new thread. Args: None. Returns: - The thread that was created to do the tracking. + The thread that was created to do the tracking (FileTracker object). """ if self.fullPath and app.prefs.editor['useBgThread']: - thread = threading.Thread(target=self.run) - thread.daemon = True # Do not continue running if main program exits. - thread.start() - return thread + self.thread = FileTracker(target=self.run) + self.thread.daemon = True # Do not continue running if main program exits. + self.thread.start() + return self.thread def updateStats(self): """ From e19b73fa17beb599d4a1faa0530532767fdd75d9 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Fri, 22 Dec 2017 15:35:55 -0800 Subject: [PATCH 18/39] Moved the saved file stats to the FileStats object and created a file which would check if the file on disk has changed --- app/actions.py | 4 ++-- app/file_stats.py | 31 +++++++++++++++++++++++++++++++ app/mutator.py | 21 +-------------------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/app/actions.py b/app/actions.py index cdaebf34..24da6b45 100644 --- a/app/actions.py +++ b/app/actions.py @@ -805,7 +805,7 @@ def fileLoad(self): app.log.info('error opening file', self.fullPath) self.setMessage('error opening file', self.fullPath) return - self.savedFileStat = self.fileStats.fileStats + self.fileStats.savedFileStat = self.fileStats.fileStats self.relativePath = os.path.relpath(self.fullPath, os.getcwd()) app.log.info('fullPath', self.fullPath) app.log.info('cwd', os.getcwd()) @@ -988,7 +988,7 @@ def fileWrite(self): # Store the file's new info self.lastChecksum, self.lastFileSize = app.history.getFileInfo( self.fileStats) - self.savedFileStat = self.fileStats.fileStats + self.fileStats.savedFileStat = self.fileStats.fileStats self.setMessage('File saved') except Exception as e: color = app.color.get('status_line_error') diff --git a/app/file_stats.py b/app/file_stats.py index 1ced4005..2f8c5ec3 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -33,6 +33,7 @@ def __init__(self, fullPath='', pollingInterval=2): # All necessary file info should be placed in this dictionary. self.fileInfo = {'isReadOnly': False, 'size': 0} + self.savedFileStat = None # Used to determine if file on disk has changed. self.statsLock = threading.Lock() self.textBuffer = None self.thread = self.startTracking() @@ -124,3 +125,33 @@ def getUpdatedFileInfo(self): def setTextBuffer(self, textBuffer): self.textBuffer = textBuffer + + def fileOnDiskChanged(self): + """ + Checks whether the file on disk has changed since we last opened/saved it. + + Args: + None. + + Returns: + True if the file on disk has changed. Otherwise, False. + """ + self.updateStats() + s1 = self.fileStats + s2 = self.savedFileStat + app.log.info('st_mode', s1.st_mode, s2.st_mode) + app.log.info('st_ino', s1.st_ino, s2.st_ino) + app.log.info('st_dev', s1.st_dev, s2.st_dev) + app.log.info('st_uid', s1.st_uid, s2.st_uid) + app.log.info('st_gid', s1.st_gid, s2.st_gid) + app.log.info('st_size', s1.st_size, s2.st_size) + app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) + app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) + return not (s1.st_mode == s2.st_mode and + s1.st_ino == s2.st_ino and + s1.st_dev == s2.st_dev and + s1.st_uid == s2.st_uid and + s1.st_gid == s2.st_gid and + s1.st_size == s2.st_size and + s1.st_mtime == s2.st_mtime and + s1.st_ctime == s2.st_ctime) \ No newline at end of file diff --git a/app/mutator.py b/app/mutator.py index 7720010b..b6bd914e 100644 --- a/app/mutator.py +++ b/app/mutator.py @@ -46,7 +46,6 @@ def __init__(self): self.fileExtension = '' self.fullPath = '' self.goalCol = 0 - self.savedFileStat = None self.penGrammar = None self.parser = None self.parserTime = .0 @@ -139,25 +138,7 @@ def isDirty(self): def isSafeToWrite(self): if not os.path.exists(self.fullPath): return True - self.fileStats.updateStats() - s1 = self.fileStats.fileStats - s2 = self.savedFileStat - app.log.info('st_mode', s1.st_mode, s2.st_mode) - app.log.info('st_ino', s1.st_ino, s2.st_ino) - app.log.info('st_dev', s1.st_dev, s2.st_dev) - app.log.info('st_uid', s1.st_uid, s2.st_uid) - app.log.info('st_gid', s1.st_gid, s2.st_gid) - app.log.info('st_size', s1.st_size, s2.st_size) - app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) - app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) - return (s1.st_mode == s2.st_mode and - s1.st_ino == s2.st_ino and - s1.st_dev == s2.st_dev and - s1.st_uid == s2.st_uid and - s1.st_gid == s2.st_gid and - s1.st_size == s2.st_size and - s1.st_mtime == s2.st_mtime and - s1.st_ctime == s2.st_ctime) + return not self.fileStats.fileOnDiskChanged() def __doMoveLines(self, begin, end, to): From 6145283d2baa4ae7b54c1f72938338413c916ac8 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Sat, 23 Dec 2017 16:07:42 -0800 Subject: [PATCH 19/39] Created new controller and window for popup messages. Still need to figure out how to render this window --- app/controller.py | 4 +++- app/cu_editor.py | 24 ++++++++++++++++++++++++ app/window.py | 29 +++++++++++++++++++++-------- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/app/controller.py b/app/controller.py index 241dc506..fc752e7e 100644 --- a/app/controller.py +++ b/app/controller.py @@ -64,6 +64,9 @@ def changeToGoto(self): def changeToPaletteWindow(self): self.host.changeFocusTo(self.host.host.paletteWindow) + def changeToPopupWindow(self): + self.host.changeFocusTo(self.host.popupWindow) + def changeToPrediction(self): self.host.changeFocusTo(self.host.interactivePrediction) @@ -253,4 +256,3 @@ def setTextBuffer(self, textBuffer): def unfocus(self): self.controller.unfocus() - diff --git a/app/cu_editor.py b/app/cu_editor.py index aff7eefb..fbffcb4b 100644 --- a/app/cu_editor.py +++ b/app/cu_editor.py @@ -397,6 +397,30 @@ def setTextBuffer(self, textBuffer): }) self.commandSet = commandSet +class PopupController(app.controller.Controller): + """ + A controller for pop up boxes to notify the user. + """ + def __init__(self, view): + app.controller.Controller.__init__(self, view, 'popup') + self.view = view + def noOp(c): + app.log.info('noOp in PopupController') + self.commandDefault = noOp + self.commandSet = { + ord('Y'): self.reloadBuffer, + ord('y'): self.reloadBuffer, + ord('N'): self.changeToHostWindow, + ord('n'): self.changeToHostWindow, + KEY_ESCAPE: self.changeToHostWindow, + } + + def changeToHostWindow(self): + self.view.hide() + app.controller.Controller.changeToHostWindow(self) + + def setTextBuffer(self, textBuffer): + self.textBuffer = textBuffer class PaletteDialogController(app.controller.Controller): """.""" diff --git a/app/window.py b/app/window.py index 2c52632e..03d0fca6 100755 --- a/app/window.py +++ b/app/window.py @@ -718,6 +718,8 @@ def __init__(self, host): if 1: self.interactiveSaveAs = LabeledLine(self, "save as: ") self.interactiveSaveAs.setController(app.cu_editor.InteractiveSaveAs) + if 1: + self.popup = Popup(self) if 1: self.topInfo = TopInfo(self) self.topInfo.setParent(self, 0) @@ -1183,6 +1185,25 @@ def setPath(self, path): self.textBuffer.selectionAll() self.textBuffer.editPasteLines((path,)) +class Popup(Window): + def __init__(self, host): + assert(host) + Window.__init__(self, host) + self.host = host + self.controller = app.cu_editor.PopupController(self) + self.setTextBuffer(app.text_buffer.TextBuffer()) + self.controller.setTextBuffer(self.textBuffer) + + def render(self): + pass + + def setTextBuffer(self, textBuffer): + Window.setTextBuffer(self, textBuffer) + self.controller.setTextBuffer(textBuffer) + + def unfocus(self): + self.hide() + Window.unfocus(self) class PaletteWindow(Window): """A window with example foreground and background text colors.""" @@ -1201,11 +1222,3 @@ def render(self): for k in range(rows): self.addStr(k, i * 5, ' %3d ' % (i + k * width,), app.color.get(i + k * width)) - - def setTextBuffer(self, textBuffer): - Window.setTextBuffer(self, textBuffer) - self.controller.setTextBuffer(textBuffer) - - def unfocus(self): - self.hide() - Window.unfocus(self) From 47c12569cdee07ab9747442f90cfbb6addae2b9a Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Sun, 24 Dec 2017 16:04:37 -0800 Subject: [PATCH 20/39] Added basic rendering and integration of the popup window --- app/controller.py | 2 +- app/cu_editor.py | 5 +++++ app/window.py | 20 +++++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/controller.py b/app/controller.py index fc752e7e..64a4500b 100644 --- a/app/controller.py +++ b/app/controller.py @@ -64,7 +64,7 @@ def changeToGoto(self): def changeToPaletteWindow(self): self.host.changeFocusTo(self.host.host.paletteWindow) - def changeToPopupWindow(self): + def changeToPopup(self): self.host.changeFocusTo(self.host.popupWindow) def changeToPrediction(self): diff --git a/app/cu_editor.py b/app/cu_editor.py index fbffcb4b..6e3065ac 100644 --- a/app/cu_editor.py +++ b/app/cu_editor.py @@ -71,6 +71,7 @@ def mainWindowCommands(controller, textBuffer): commands.update({ KEY_ESCAPE: textBuffer.normalize, KEY_F1: controller.info, + KEY_F4: controller.changeToPopup, KEY_BTAB: textBuffer.unindent, KEY_PAGE_UP: textBuffer.cursorSelectNonePageUp, KEY_PAGE_DOWN: textBuffer.cursorSelectNonePageDown, @@ -422,6 +423,10 @@ def changeToHostWindow(self): def setTextBuffer(self, textBuffer): self.textBuffer = textBuffer + def reloadBuffer(self): + mainBuffer = self.view.host.textBuffer + mainBuffer.fileLoad() + class PaletteDialogController(app.controller.Controller): """.""" def __init__(self, view): diff --git a/app/window.py b/app/window.py index 03d0fca6..c5dfad72 100755 --- a/app/window.py +++ b/app/window.py @@ -1193,9 +1193,27 @@ def __init__(self, host): self.controller = app.cu_editor.PopupController(self) self.setTextBuffer(app.text_buffer.TextBuffer()) self.controller.setTextBuffer(self.textBuffer) + self.message = [] def render(self): - pass + width = 30 + rows = len(self.message) + 2 + for row in range(rows): + if row == 0 or row == rows - 1: + self.addStr(row, 0, ' ' * width) + else: + self.addStr(row, 0, ' %s ' % self.message[row]) + + def setMessage(self, message): + """ + Sets the Popup window's message to the given message. + + message (str): A string that you want to display. + + Returns: + None. + """ + self.message = message.split("\n") def setTextBuffer(self, textBuffer): Window.setTextBuffer(self, textBuffer) From 237c80234c696b7c6030eef57f6c1d2b790dc93a Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Tue, 23 Jan 2018 08:28:27 -0800 Subject: [PATCH 21/39] Fixed the check condition for the FileStats thread to detect file changes better --- app/file_stats.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/file_stats.py b/app/file_stats.py index 2f8c5ec3..ca52f95a 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -46,7 +46,8 @@ def run(self): newFileIsReadOnly = self.getUpdatedFileInfo()['isReadOnly'] program = self.textBuffer.view.host turnoverTime = 0 - if newFileIsReadOnly != oldFileIsReadOnly and program: + if (newFileIsReadOnly != oldFileIsReadOnly or + self.fileOnDiskChanged()) and program: before = time.time() app.background.bg.put((program, 'redraw', self.thread.semaphore)) # Wait for bg thread to finish refreshing before sleeping From 4afcef4d3204f47d6cffa8d2da3d9fdd3e80467a Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Sun, 24 Dec 2017 17:56:07 -0800 Subject: [PATCH 22/39] Added check that prevents this function from breaking before some attributes are initialized --- app/file_stats.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/app/file_stats.py b/app/file_stats.py index ca52f95a..fa200f25 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -137,22 +137,23 @@ def fileOnDiskChanged(self): Returns: True if the file on disk has changed. Otherwise, False. """ - self.updateStats() - s1 = self.fileStats - s2 = self.savedFileStat - app.log.info('st_mode', s1.st_mode, s2.st_mode) - app.log.info('st_ino', s1.st_ino, s2.st_ino) - app.log.info('st_dev', s1.st_dev, s2.st_dev) - app.log.info('st_uid', s1.st_uid, s2.st_uid) - app.log.info('st_gid', s1.st_gid, s2.st_gid) - app.log.info('st_size', s1.st_size, s2.st_size) - app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) - app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) - return not (s1.st_mode == s2.st_mode and - s1.st_ino == s2.st_ino and - s1.st_dev == s2.st_dev and - s1.st_uid == s2.st_uid and - s1.st_gid == s2.st_gid and - s1.st_size == s2.st_size and - s1.st_mtime == s2.st_mtime and - s1.st_ctime == s2.st_ctime) \ No newline at end of file + if (self.updateStats() and self.fileStats): + s1 = self.fileStats + s2 = self.savedFileStat + app.log.info('st_mode', s1.st_mode, s2.st_mode) + app.log.info('st_ino', s1.st_ino, s2.st_ino) + app.log.info('st_dev', s1.st_dev, s2.st_dev) + app.log.info('st_uid', s1.st_uid, s2.st_uid) + app.log.info('st_gid', s1.st_gid, s2.st_gid) + app.log.info('st_size', s1.st_size, s2.st_size) + app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) + app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) + return not (s1.st_mode == s2.st_mode and + s1.st_ino == s2.st_ino and + s1.st_dev == s2.st_dev and + s1.st_uid == s2.st_uid and + s1.st_gid == s2.st_gid and + s1.st_size == s2.st_size and + s1.st_mtime == s2.st_mtime and + s1.st_ctime == s2.st_ctime) + return False \ No newline at end of file From 502ebcdcf05ece4b2ce392e7e7140dc2c2219356 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Sun, 24 Dec 2017 20:13:15 -0800 Subject: [PATCH 23/39] Added better support for changing files to track and can check if file on disk has changed --- app/buffer_manager.py | 5 +-- app/file_stats.py | 76 ++++++++++++++++++------------------------- app/text_buffer.py | 18 ++++++++++ 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/app/buffer_manager.py b/app/buffer_manager.py index ea5fc139..9f4487d8 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -93,9 +93,6 @@ def loadTextBuffer(self, relPath, view): textBuffer = app.text_buffer.TextBuffer() textBuffer.view = view self.renameBuffer(textBuffer, fullPath) - # Make this text buffer track the file its in charge of. - textBuffer.fileStats.setTextBuffer(textBuffer) - textBuffer.fileStats.changeMonitoredFile(fullPath) textBuffer.fileLoad() self.buffers.append(textBuffer) if 0: @@ -141,7 +138,7 @@ def renameBuffer(self, fileBuffer, fullPath): # TODO(dschuyler): this can be phased out. It was from a time when the # buffer manager needed to know if a path changed. fileBuffer.fullPath = fullPath - fileBuffer.fileStats.changeMonitoredFile(fullPath) + fileBuffer.changeFileStats() # Track this new file. def fileClose(self, path): pass diff --git a/app/file_stats.py b/app/file_stats.py index fa200f25..a9e4d49b 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -36,7 +36,7 @@ def __init__(self, fullPath='', pollingInterval=2): self.savedFileStat = None # Used to determine if file on disk has changed. self.statsLock = threading.Lock() self.textBuffer = None - self.thread = self.startTracking() + self.thread = None self.updateStats() def run(self): @@ -55,24 +55,6 @@ def run(self): turnoverTime = time.time() - before time.sleep(max(self.pollingInterval - turnoverTime, 0)) - def changeMonitoredFile(self, fullPath): - """ - Stops tracking whatever file this object was monitoring before and tracks - the newly specified file. The self.textBuffer attribute must - be set in order for the created thread to work properly. - - Args: - None. - - Returns: - None. - """ - if self.thread: - self.thread.shouldExit = True - self.fullPath = fullPath - self.updateStats() - self.startTracking() - def startTracking(self): """ Starts tracking the file whose path is specified in self.fullPath. Sets @@ -84,11 +66,10 @@ def startTracking(self): Returns: The thread that was created to do the tracking (FileTracker object). """ - if self.fullPath and app.prefs.editor['useBgThread']: - self.thread = FileTracker(target=self.run) - self.thread.daemon = True # Do not continue running if main program exits. - self.thread.start() - return self.thread + self.thread = FileTracker(target=self.run) + self.thread.daemon = True # Do not continue running if main program exits. + self.thread.start() + return self.thread def updateStats(self): """ @@ -137,23 +118,30 @@ def fileOnDiskChanged(self): Returns: True if the file on disk has changed. Otherwise, False. """ - if (self.updateStats() and self.fileStats): - s1 = self.fileStats - s2 = self.savedFileStat - app.log.info('st_mode', s1.st_mode, s2.st_mode) - app.log.info('st_ino', s1.st_ino, s2.st_ino) - app.log.info('st_dev', s1.st_dev, s2.st_dev) - app.log.info('st_uid', s1.st_uid, s2.st_uid) - app.log.info('st_gid', s1.st_gid, s2.st_gid) - app.log.info('st_size', s1.st_size, s2.st_size) - app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) - app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) - return not (s1.st_mode == s2.st_mode and - s1.st_ino == s2.st_ino and - s1.st_dev == s2.st_dev and - s1.st_uid == s2.st_uid and - s1.st_gid == s2.st_gid and - s1.st_size == s2.st_size and - s1.st_mtime == s2.st_mtime and - s1.st_ctime == s2.st_ctime) - return False \ No newline at end of file + try: + if (self.updateStats() and self.fileStats): + s1 = self.fileStats + s2 = self.savedFileStat + app.log.info('st_mode', s1.st_mode, s2.st_mode) + app.log.info('st_ino', s1.st_ino, s2.st_ino) + app.log.info('st_dev', s1.st_dev, s2.st_dev) + app.log.info('st_uid', s1.st_uid, s2.st_uid) + app.log.info('st_gid', s1.st_gid, s2.st_gid) + app.log.info('st_size', s1.st_size, s2.st_size) + app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) + app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) + return not (s1.st_mode == s2.st_mode and + s1.st_ino == s2.st_ino and + s1.st_dev == s2.st_dev and + s1.st_uid == s2.st_uid and + s1.st_gid == s2.st_gid and + s1.st_size == s2.st_size and + s1.st_mtime == s2.st_mtime and + s1.st_ctime == s2.st_ctime) + return False + except Exception as e: + print(e) + + def cleanup(self): + if self.thread: + self.thread.shouldExit = True \ No newline at end of file diff --git a/app/text_buffer.py b/app/text_buffer.py index 57018364..16245c16 100644 --- a/app/text_buffer.py +++ b/app/text_buffer.py @@ -39,6 +39,24 @@ def __init__(self): self.bookmarks = [] self.nextBookmarkColorPos = 0 + def changeFileStats(self): + """ + Stops tracking whatever file this object was monitoring before and tracks + the file whose absolute path is defined in self.fullPath. + + Args: + None. + + Returns: + None. + """ + self.fileStats.cleanup() + self.fileStats = app.file_stats.FileStats(self.fullPath) + self.fileStats.setTextBuffer(self) + self.fileStats.updateStats() + if app.prefs.editor['useBgThread']: + self.fileStats.startTracking() + def checkScrollToCursor(self, window): """Move the selected view rectangle so that the cursor is visible.""" maxRow, maxCol = window.rows, window.cols From 085addd822df10b154b20fa0bb458a3a72b774b4 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Sun, 31 Dec 2017 14:11:19 -0800 Subject: [PATCH 24/39] Created a new function that checks if a file's content has changed. This is now used over the old function which also checked other stats --- app/file_stats.py | 39 ++++++++++++++++++++++++++++++++++----- app/mutator.py | 2 +- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/app/file_stats.py b/app/file_stats.py index a9e4d49b..1fcd74a3 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -46,11 +46,20 @@ def run(self): newFileIsReadOnly = self.getUpdatedFileInfo()['isReadOnly'] program = self.textBuffer.view.host turnoverTime = 0 - if (newFileIsReadOnly != oldFileIsReadOnly or - self.fileOnDiskChanged()) and program: + redraw = False + if program: + if newFileIsReadOnly != oldFileIsReadOnly: + print(1) + redraw = True + if self.fileContentOnDiskChanged(): + print(2) + redraw = True + if redraw: + print(3) before = time.time() + # Send a redraw request. app.background.bg.put((program, 'redraw', self.thread.semaphore)) - # Wait for bg thread to finish refreshing before sleeping + # Wait for bg thread to finish refreshing before sleeping. self.thread.semaphore.acquire() turnoverTime = time.time() - before time.sleep(max(self.pollingInterval - turnoverTime, 0)) @@ -111,6 +120,8 @@ def setTextBuffer(self, textBuffer): def fileOnDiskChanged(self): """ Checks whether the file on disk has changed since we last opened/saved it. + This includes checking its permission bits, modified time, metadata modified + time, file size, and other statistics. Args: None. @@ -122,14 +133,12 @@ def fileOnDiskChanged(self): if (self.updateStats() and self.fileStats): s1 = self.fileStats s2 = self.savedFileStat - app.log.info('st_mode', s1.st_mode, s2.st_mode) app.log.info('st_ino', s1.st_ino, s2.st_ino) app.log.info('st_dev', s1.st_dev, s2.st_dev) app.log.info('st_uid', s1.st_uid, s2.st_uid) app.log.info('st_gid', s1.st_gid, s2.st_gid) app.log.info('st_size', s1.st_size, s2.st_size) app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) - app.log.info('st_ctime', s1.st_ctime, s2.st_ctime) return not (s1.st_mode == s2.st_mode and s1.st_ino == s2.st_ino and s1.st_dev == s2.st_dev and @@ -142,6 +151,26 @@ def fileOnDiskChanged(self): except Exception as e: print(e) + def fileContentOnDiskChanged(self): + """ + Checks if a file has been modified since we last opened/saved it. + + Args: + None. + + Returns: + True if the file has been modified. Otherwise, False. + """ + try: + if (self.updateStats() and self.fileStats): + s1 = self.fileStats + s2 = self.savedFileStat + app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) + return not s1.st_mtime == s2.st_mtime + return False + except Exception as e: + print(e) + def cleanup(self): if self.thread: self.thread.shouldExit = True \ No newline at end of file diff --git a/app/mutator.py b/app/mutator.py index b6bd914e..dfd7b5bb 100644 --- a/app/mutator.py +++ b/app/mutator.py @@ -138,7 +138,7 @@ def isDirty(self): def isSafeToWrite(self): if not os.path.exists(self.fullPath): return True - return not self.fileStats.fileOnDiskChanged() + return not self.fileStats.fileContentOnDiskChanged() def __doMoveLines(self, begin, end, to): From ffcb070f9d3c202ef10aa0fa3f7de59d68e526bd Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Sun, 31 Dec 2017 22:10:32 -0800 Subject: [PATCH 25/39] Got popup window to kind of display. Need a lot of polishing --- app/background.py | 8 ++++++++ app/ci_program.py | 16 +++++++++++----- app/curses_util.py | 2 ++ app/file_stats.py | 1 + app/window.py | 8 +++----- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/background.py b/app/background.py index 3df188ea..6d23b847 100644 --- a/app/background.py +++ b/app/background.py @@ -77,6 +77,14 @@ def redrawProgram(program): redrawProgram(program) callerSemaphore.release() continue + elif message == 'popup': + app.log.meta('bg received popup message') + # assert(callerSemaphore != None) + pid = os.getpid() + signalNumber = signal.SIGUSR1 + outputQueue.put(('popup', None)) + os.kill(pid, signalNumber) + continue program.executeCommandList(message) redrawProgram(program) #app.profile.endPythonProfile(profile) diff --git a/app/ci_program.py b/app/ci_program.py index 6bae3ad1..01acced3 100755 --- a/app/ci_program.py +++ b/app/ci_program.py @@ -129,7 +129,11 @@ def commandLoop(self): userMessage(line[:-1]) self.exiting = True return - self.refresh(frame[0], frame[1]) + if frame[0] == 'popup': + self.popupWindow.setMessage("Hi there friend\nHow are you doing today?") + self.changeFocusTo(self.popupWindow) + else: + self.refresh(frame[0], frame[1]) elif 1: frame = app.render.frame.grabFrame() self.refresh(frame[0], frame[1]) @@ -209,7 +213,6 @@ def commandLoop(self): ch = app.curses_util.UNICODE_INPUT if ch == 0 and useBgThread: # bg response. - frame = None while self.bg.hasMessage(): frame = self.bg.get() if frame[0] == 'exception': @@ -217,8 +220,11 @@ def commandLoop(self): userMessage(line[:-1]) self.exiting = True return - if frame is not None: - self.refresh(frame[0], frame[1]) + if frame[0] == 'popup': + self.popupWindow.setMessage("Hi there friend\nHow are you doing today?") + self.changeFocusTo(self.popupWindow) + else: + self.refresh(frame[0], frame[1]) elif ch != curses.ERR: self.ch = ch if ch == curses.KEY_MOUSE: @@ -284,7 +290,7 @@ def startup(self): self.debugWindow = None self.debugUndoWindow = None self.logWindow = None - self.paletteWindow = None + self.popupWindow = app.window.PopupWindow(self) self.paletteWindow = app.window.PaletteWindow(self) self.inputWindow = app.window.InputWindow(self) self.zOrder.append(self.inputWindow) diff --git a/app/curses_util.py b/app/curses_util.py index f011491d..67c25ebc 100644 --- a/app/curses_util.py +++ b/app/curses_util.py @@ -218,5 +218,7 @@ def windowChangedHandler(signum, frame): curses.ungetch(curses.KEY_RESIZE) signal.signal(signal.SIGWINCH, windowChangedHandler) def wakeGetch(signum, frame): + import app.log + app.log.meta("unget(0). caught signal") curses.ungetch(0) signal.signal(signal.SIGUSR1, wakeGetch) diff --git a/app/file_stats.py b/app/file_stats.py index 1fcd74a3..9cb105df 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -53,6 +53,7 @@ def run(self): redraw = True if self.fileContentOnDiskChanged(): print(2) + app.background.bg.put((program, 'popup', None)) redraw = True if redraw: print(3) diff --git a/app/window.py b/app/window.py index c5dfad72..e24a39a9 100755 --- a/app/window.py +++ b/app/window.py @@ -718,8 +718,6 @@ def __init__(self, host): if 1: self.interactiveSaveAs = LabeledLine(self, "save as: ") self.interactiveSaveAs.setController(app.cu_editor.InteractiveSaveAs) - if 1: - self.popup = Popup(self) if 1: self.topInfo = TopInfo(self) self.topInfo.setParent(self, 0) @@ -1185,7 +1183,7 @@ def setPath(self, path): self.textBuffer.selectionAll() self.textBuffer.editPasteLines((path,)) -class Popup(Window): +class PopupWindow(Window): def __init__(self, host): assert(host) Window.__init__(self, host) @@ -1200,9 +1198,9 @@ def render(self): rows = len(self.message) + 2 for row in range(rows): if row == 0 or row == rows - 1: - self.addStr(row, 0, ' ' * width) + self.addStr(row, 0, ' ' * width, app.color.get(0)) else: - self.addStr(row, 0, ' %s ' % self.message[row]) + self.addStr(row, 0, ' %s ' % self.message[row - 1], app.color.get(0)) def setMessage(self, message): """ From 799d68137c1271fe036c4ad1c247c2d2486fde5d Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Sun, 31 Dec 2017 23:15:14 -0800 Subject: [PATCH 26/39] Fixed positioning of the popup window. Need to fix format and coloring of it as well as stop polling when a popup appears --- app/file_stats.py | 3 --- app/window.py | 11 +++++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/file_stats.py b/app/file_stats.py index 9cb105df..b2009d5f 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -49,14 +49,11 @@ def run(self): redraw = False if program: if newFileIsReadOnly != oldFileIsReadOnly: - print(1) redraw = True if self.fileContentOnDiskChanged(): - print(2) app.background.bg.put((program, 'popup', None)) redraw = True if redraw: - print(3) before = time.time() # Send a redraw request. app.background.bg.put((program, 'redraw', self.thread.semaphore)) diff --git a/app/window.py b/app/window.py index e24a39a9..7b47a548 100755 --- a/app/window.py +++ b/app/window.py @@ -1194,13 +1194,16 @@ def __init__(self, host): self.message = [] def render(self): - width = 30 - rows = len(self.message) + 2 + maxRows, maxCols = self.host.rows, self.host.cols + cols = min(30, maxCols) + rows = min(len(self.message) + 2, maxRows) + self.resizeTo(rows, cols) + self.moveTo(maxRows / 2 - rows / 2, maxCols / 2 - cols / 2) for row in range(rows): if row == 0 or row == rows - 1: - self.addStr(row, 0, ' ' * width, app.color.get(0)) + self.addStr(row, 0, ' ' * cols, app.color.get(70)) else: - self.addStr(row, 0, ' %s ' % self.message[row - 1], app.color.get(0)) + self.addStr(row, 0, ' %s ' % self.message[row - 1], app.color.get(70)) def setMessage(self, message): """ From 4661eda4eeae280cf8ef62ce442481490cfc5264 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Mon, 1 Jan 2018 00:44:49 -0800 Subject: [PATCH 27/39] Fixed the layout of the popup textbox. Need to fix color and add Y/N prompt --- app/cu_editor.py | 9 +++++---- app/window.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/cu_editor.py b/app/cu_editor.py index 6e3065ac..49700771 100644 --- a/app/cu_editor.py +++ b/app/cu_editor.py @@ -405,7 +405,7 @@ class PopupController(app.controller.Controller): def __init__(self, view): app.controller.Controller.__init__(self, view, 'popup') self.view = view - def noOp(c): + def noOp(ch, meta): app.log.info('noOp in PopupController') self.commandDefault = noOp self.commandSet = { @@ -416,15 +416,16 @@ def noOp(c): KEY_ESCAPE: self.changeToHostWindow, } - def changeToHostWindow(self): + def changeToMainWindow(self): self.view.hide() - app.controller.Controller.changeToHostWindow(self) + mainProgram = self.host.host + mainProgram.changeFocusTo(mainProgram.inputWindow) def setTextBuffer(self, textBuffer): self.textBuffer = textBuffer def reloadBuffer(self): - mainBuffer = self.view.host.textBuffer + mainBuffer = self.view.host.inputWindow.textBuffer mainBuffer.fileLoad() class PaletteDialogController(app.controller.Controller): diff --git a/app/window.py b/app/window.py index 7b47a548..9394c2b2 100755 --- a/app/window.py +++ b/app/window.py @@ -1191,11 +1191,12 @@ def __init__(self, host): self.controller = app.cu_editor.PopupController(self) self.setTextBuffer(app.text_buffer.TextBuffer()) self.controller.setTextBuffer(self.textBuffer) + self.longestLineLength = 0 self.message = [] def render(self): maxRows, maxCols = self.host.rows, self.host.cols - cols = min(30, maxCols) + cols = min(self.longestLineLength + 6, maxCols) rows = min(len(self.message) + 2, maxRows) self.resizeTo(rows, cols) self.moveTo(maxRows / 2 - rows / 2, maxCols / 2 - cols / 2) @@ -1203,7 +1204,12 @@ def render(self): if row == 0 or row == rows - 1: self.addStr(row, 0, ' ' * cols, app.color.get(70)) else: - self.addStr(row, 0, ' %s ' % self.message[row - 1], app.color.get(70)) + lineLength = len(self.message[row - 1]) + spacing1 = (cols - lineLength) / 2 + spacing2 = cols - lineLength - spacing1 + self.addStr(row,0, + ' ' * spacing1 + self.message[row - 1] + ' ' * spacing2, + app.color.get(70)) def setMessage(self, message): """ @@ -1215,6 +1221,7 @@ def setMessage(self, message): None. """ self.message = message.split("\n") + self.longestLineLength = max([len(line) for line in self.message]) def setTextBuffer(self, textBuffer): Window.setTextBuffer(self, textBuffer) From 048ad101f274a4d5ebd3f9d4132bdc1dec022b53 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Tue, 2 Jan 2018 10:47:58 -0800 Subject: [PATCH 28/39] Add comment --- app/window.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/window.py b/app/window.py index 9394c2b2..d5db954e 100755 --- a/app/window.py +++ b/app/window.py @@ -1195,6 +1195,9 @@ def __init__(self, host): self.message = [] def render(self): + """ + Display a box of text in the center of the window. + """ maxRows, maxCols = self.host.rows, self.host.cols cols = min(self.longestLineLength + 6, maxCols) rows = min(len(self.message) + 2, maxRows) From 2f575d976b9c3aac19f8af2306f78849eccf0544 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Wed, 3 Jan 2018 10:18:12 -0800 Subject: [PATCH 29/39] Added options parameter for the popup window, improved the format of the popup, and fixed the popup being able to go back to the main window. --- app/cu_editor.py | 6 +++--- app/window.py | 22 ++++++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/cu_editor.py b/app/cu_editor.py index 49700771..f016b480 100644 --- a/app/cu_editor.py +++ b/app/cu_editor.py @@ -411,9 +411,9 @@ def noOp(ch, meta): self.commandSet = { ord('Y'): self.reloadBuffer, ord('y'): self.reloadBuffer, - ord('N'): self.changeToHostWindow, - ord('n'): self.changeToHostWindow, - KEY_ESCAPE: self.changeToHostWindow, + ord('N'): self.changeToMainWindow, + ord('n'): self.changeToMainWindow, + KEY_ESCAPE: self.changeToMainWindow, } def changeToMainWindow(self): diff --git a/app/window.py b/app/window.py index d5db954e..0b93cfc0 100755 --- a/app/window.py +++ b/app/window.py @@ -1193,6 +1193,8 @@ def __init__(self, host): self.controller.setTextBuffer(self.textBuffer) self.longestLineLength = 0 self.message = [] + self.showOptions = True + self.options = ["Y", "N"] def render(self): """ @@ -1200,19 +1202,23 @@ def render(self): """ maxRows, maxCols = self.host.rows, self.host.cols cols = min(self.longestLineLength + 6, maxCols) - rows = min(len(self.message) + 2, maxRows) + rows = min(len(self.message) + 4, maxRows) self.resizeTo(rows, cols) self.moveTo(maxRows / 2 - rows / 2, maxCols / 2 - cols / 2) for row in range(rows): - if row == 0 or row == rows - 1: + if row == rows - 2 and self.showOptions: + message = '/'.join(self.options) + elif row == 0 or row >= rows - 3: self.addStr(row, 0, ' ' * cols, app.color.get(70)) + continue else: - lineLength = len(self.message[row - 1]) - spacing1 = (cols - lineLength) / 2 - spacing2 = cols - lineLength - spacing1 - self.addStr(row,0, - ' ' * spacing1 + self.message[row - 1] + ' ' * spacing2, - app.color.get(70)) + message = self.message[row - 1] + lineLength = len(message) + spacing1 = (cols - lineLength) / 2 + spacing2 = cols - lineLength - spacing1 + self.addStr(row, 0, + ' ' * spacing1 + message + ' ' * spacing2, + app.color.get(70)) def setMessage(self, message): """ From decf88f5ccd9d88071f48ca5023200a639d1d1bd Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Wed, 3 Jan 2018 11:53:54 -0800 Subject: [PATCH 30/39] Stopped fileStats thread when waiting for user input. Need to make it so popups don't show up again if user said to not reload buffer. --- app/ci_program.py | 2 -- app/cu_editor.py | 11 +++++++++++ app/curses_util.py | 1 - app/file_stats.py | 12 ++++++++---- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/ci_program.py b/app/ci_program.py index 01acced3..a31d5e0c 100755 --- a/app/ci_program.py +++ b/app/ci_program.py @@ -130,7 +130,6 @@ def commandLoop(self): self.exiting = True return if frame[0] == 'popup': - self.popupWindow.setMessage("Hi there friend\nHow are you doing today?") self.changeFocusTo(self.popupWindow) else: self.refresh(frame[0], frame[1]) @@ -221,7 +220,6 @@ def commandLoop(self): self.exiting = True return if frame[0] == 'popup': - self.popupWindow.setMessage("Hi there friend\nHow are you doing today?") self.changeFocusTo(self.popupWindow) else: self.refresh(frame[0], frame[1]) diff --git a/app/cu_editor.py b/app/cu_editor.py index f016b480..ebbb3438 100644 --- a/app/cu_editor.py +++ b/app/cu_editor.py @@ -405,6 +405,7 @@ class PopupController(app.controller.Controller): def __init__(self, view): app.controller.Controller.__init__(self, view, 'popup') self.view = view + self.callerSemaphore = None def noOp(ch, meta): app.log.info('noOp in PopupController') self.commandDefault = noOp @@ -420,13 +421,23 @@ def changeToMainWindow(self): self.view.hide() mainProgram = self.host.host mainProgram.changeFocusTo(mainProgram.inputWindow) + self.callerSemaphore.release() def setTextBuffer(self, textBuffer): self.textBuffer = textBuffer def reloadBuffer(self): + """ + Reloads the file on disk into the program. This will get rid of all changes + that have been made to the current file. This will also remove all + edit history. + + TODO: Make this reloading a new change that can be appended + to the redo chain so user can undo out of a reloadBuffer call. + """ mainBuffer = self.view.host.inputWindow.textBuffer mainBuffer.fileLoad() + self.changeToMainWindow() class PaletteDialogController(app.controller.Controller): """.""" diff --git a/app/curses_util.py b/app/curses_util.py index 67c25ebc..b7280d6c 100644 --- a/app/curses_util.py +++ b/app/curses_util.py @@ -219,6 +219,5 @@ def windowChangedHandler(signum, frame): signal.signal(signal.SIGWINCH, windowChangedHandler) def wakeGetch(signum, frame): import app.log - app.log.meta("unget(0). caught signal") curses.ungetch(0) signal.signal(signal.SIGUSR1, wakeGetch) diff --git a/app/file_stats.py b/app/file_stats.py index b2009d5f..0bc1ac01 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -47,20 +47,24 @@ def run(self): program = self.textBuffer.view.host turnoverTime = 0 redraw = False + waitOnSemaphore = False if program: if newFileIsReadOnly != oldFileIsReadOnly: redraw = True if self.fileContentOnDiskChanged(): + program.popupWindow.setMessage( + "The file on disk has changed.\nReload file?") + program.popupWindow.controller.callerSemaphore = self.thread.semaphore app.background.bg.put((program, 'popup', None)) redraw = True + waitOnSemaphore = True if redraw: - before = time.time() # Send a redraw request. app.background.bg.put((program, 'redraw', self.thread.semaphore)) - # Wait for bg thread to finish refreshing before sleeping. self.thread.semaphore.acquire() - turnoverTime = time.time() - before - time.sleep(max(self.pollingInterval - turnoverTime, 0)) + if waitOnSemaphore: + self.thread.semaphore.acquire() # Wait for user to respond to popup. + time.sleep(self.pollingInterval) def startTracking(self): """ From 105d9abdd3a0231df359d6345e6642ccd75e89e5 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Wed, 3 Jan 2018 19:12:34 -0800 Subject: [PATCH 31/39] Made new functions that check whether file has changed since save and since last check --- app/file_stats.py | 42 ++++++++++++++++++++++++++++++++---------- app/mutator.py | 2 +- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/app/file_stats.py b/app/file_stats.py index 0bc1ac01..8e389ead 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -28,12 +28,15 @@ def __init__(self, fullPath='', pollingInterval=2): pollingInterval (float): The frequency at which you want to poll the file. """ self.fullPath = fullPath + # The stats of the file since we last checked it. This should be + # the most updated version of the file's stats. self.fileStats = None self.pollingInterval = pollingInterval # All necessary file info should be placed in this dictionary. self.fileInfo = {'isReadOnly': False, 'size': 0} - self.savedFileStat = None # Used to determine if file on disk has changed. + # Used to determine if file on disk has changed since the last save + self.savedFileStat = None self.statsLock = threading.Lock() self.textBuffer = None self.thread = None @@ -41,23 +44,22 @@ def __init__(self, fullPath='', pollingInterval=2): def run(self): while not self.thread.shouldExit: - # Redraw the screen if the file changed READ ONLY permissions. oldFileIsReadOnly = self.fileInfo['isReadOnly'] - newFileIsReadOnly = self.getUpdatedFileInfo()['isReadOnly'] program = self.textBuffer.view.host - turnoverTime = 0 redraw = False waitOnSemaphore = False if program: - if newFileIsReadOnly != oldFileIsReadOnly: - redraw = True - if self.fileContentOnDiskChanged(): + if self.fileContentChangedSinceCheck(): program.popupWindow.setMessage( "The file on disk has changed.\nReload file?") program.popupWindow.controller.callerSemaphore = self.thread.semaphore app.background.bg.put((program, 'popup', None)) redraw = True waitOnSemaphore = True + # Check if file read permissions have changed. + newFileIsReadOnly = self.fileInfo['isReadOnly'] + if newFileIsReadOnly != oldFileIsReadOnly: + redraw = True if redraw: # Send a redraw request. app.background.bg.put((program, 'redraw', self.thread.semaphore)) @@ -119,7 +121,7 @@ def getUpdatedFileInfo(self): def setTextBuffer(self, textBuffer): self.textBuffer = textBuffer - def fileOnDiskChanged(self): + def fileChangedSinceSave(self): """ Checks whether the file on disk has changed since we last opened/saved it. This includes checking its permission bits, modified time, metadata modified @@ -153,9 +155,29 @@ def fileOnDiskChanged(self): except Exception as e: print(e) - def fileContentOnDiskChanged(self): + def fileContentChangedSinceCheck(self): + """ + Checks if a file has been modified since we last checked it from disk. + + Args: + None. + + Returns: + True if the file has been modified. Otherwise, False. + """ + try: + s1 = self.fileStats + if (self.updateStats() and self.fileStats): + s2 = self.fileStats + app.log.info('st_mtime', s1.st_mtime, s2.st_mtime) + return not s1.st_mtime == s2.st_mtime + return False + except Exception as e: + print(e) + + def fileContentChangedSinceSave(self): """ - Checks if a file has been modified since we last opened/saved it. + Checks if a file has been modified since we last saved the file. Args: None. diff --git a/app/mutator.py b/app/mutator.py index dfd7b5bb..558141d8 100644 --- a/app/mutator.py +++ b/app/mutator.py @@ -138,7 +138,7 @@ def isDirty(self): def isSafeToWrite(self): if not os.path.exists(self.fullPath): return True - return not self.fileStats.fileContentOnDiskChanged() + return not self.fileStats.fileContentChangedSinceSave() def __doMoveLines(self, begin, end, to): From 0c3565147dc6cddab9b394b688fdb9b3a6c289ec Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 4 Jan 2018 10:12:44 -0800 Subject: [PATCH 32/39] Background output now contains objects of (frame, callerSemaphore). Fixed synchronization issues for displaying popups --- app/background.py | 17 ++++++++--------- app/ci_program.py | 10 ++++++++-- app/controller.py | 2 +- app/cu_editor.py | 5 +++-- app/file_stats.py | 5 +++-- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/app/background.py b/app/background.py index 6d23b847..751767b4 100644 --- a/app/background.py +++ b/app/background.py @@ -46,7 +46,7 @@ def put(self, data): def background(inputQueue, outputQueue): - def redrawProgram(program): + def redrawProgram(program, callerSemaphore): """ Sends a SIGUSR1 signal to the current program and draws its screen. @@ -59,7 +59,7 @@ def redrawProgram(program): pid = os.getpid() signalNumber = signal.SIGUSR1 program.render() - outputQueue.put(app.render.frame.grabFrame()) + outputQueue.put((app.render.frame.grabFrame(), callerSemaphore)) os.kill(pid, signalNumber) block = True @@ -74,19 +74,18 @@ def redrawProgram(program): elif message == 'redraw': app.log.info('bg received redraw message') assert(callerSemaphore != None) - redrawProgram(program) - callerSemaphore.release() + redrawProgram(program, callerSemaphore) continue elif message == 'popup': app.log.meta('bg received popup message') # assert(callerSemaphore != None) pid = os.getpid() signalNumber = signal.SIGUSR1 - outputQueue.put(('popup', None)) + outputQueue.put((('popup', None), callerSemaphore)) os.kill(pid, signalNumber) continue program.executeCommandList(message) - redrawProgram(program) + redrawProgram(program, callerSemaphore) #app.profile.endPythonProfile(profile) if not inputQueue.empty(): continue @@ -99,16 +98,16 @@ def redrawProgram(program): program.focusedWindow.textBuffer.parseDocument() block = len(tb.parser.rows) >= len(tb.lines) if block: - redrawProgram(program) + redrawProgram(program, callerSemaphore) except Exception as e: app.log.exception(e) app.log.error('bg thread exception', e) errorType, value, tracebackInfo = sys.exc_info() out = traceback.format_exception(errorType, value, tracebackInfo) - outputQueue.put(('exception', out)) + outputQueue.put((('exception', out), None)) os.kill(os.getpid(), signal.SIGUSR1) while True: - program, message = inputQueue.get() + program, message, callerSemaphore = inputQueue.get() if message == 'quit': app.log.info('bg received quit message') return diff --git a/app/ci_program.py b/app/ci_program.py index a31d5e0c..7cd11309 100755 --- a/app/ci_program.py +++ b/app/ci_program.py @@ -123,7 +123,7 @@ def commandLoop(self): while not self.exiting: if useBgThread: while self.bg.hasMessage(): - frame = self.bg.get() + frame, callerSemaphore = self.bg.get() if frame[0] == 'exception': for line in frame[1]: userMessage(line[:-1]) @@ -131,8 +131,11 @@ def commandLoop(self): return if frame[0] == 'popup': self.changeFocusTo(self.popupWindow) + callerSemaphore.release() else: self.refresh(frame[0], frame[1]) + if callerSemaphore: + callerSemaphore.release() elif 1: frame = app.render.frame.grabFrame() self.refresh(frame[0], frame[1]) @@ -213,7 +216,7 @@ def commandLoop(self): if ch == 0 and useBgThread: # bg response. while self.bg.hasMessage(): - frame = self.bg.get() + frame, callerSemaphore = self.bg.get() if frame[0] == 'exception': for line in frame[1]: userMessage(line[:-1]) @@ -221,8 +224,11 @@ def commandLoop(self): return if frame[0] == 'popup': self.changeFocusTo(self.popupWindow) + callerSemaphore.release() else: self.refresh(frame[0], frame[1]) + if callerSemaphore: + callerSemaphore.release() elif ch != curses.ERR: self.ch = ch if ch == curses.KEY_MOUSE: diff --git a/app/controller.py b/app/controller.py index 64a4500b..daddce6d 100644 --- a/app/controller.py +++ b/app/controller.py @@ -65,7 +65,7 @@ def changeToPaletteWindow(self): self.host.changeFocusTo(self.host.host.paletteWindow) def changeToPopup(self): - self.host.changeFocusTo(self.host.popupWindow) + self.host.changeFocusTo(self.host.host.popupWindow) def changeToPrediction(self): self.host.changeFocusTo(self.host.interactivePrediction) diff --git a/app/cu_editor.py b/app/cu_editor.py index ebbb3438..a9be7768 100644 --- a/app/cu_editor.py +++ b/app/cu_editor.py @@ -71,7 +71,6 @@ def mainWindowCommands(controller, textBuffer): commands.update({ KEY_ESCAPE: textBuffer.normalize, KEY_F1: controller.info, - KEY_F4: controller.changeToPopup, KEY_BTAB: textBuffer.unindent, KEY_PAGE_UP: textBuffer.cursorSelectNonePageUp, KEY_PAGE_DOWN: textBuffer.cursorSelectNonePageDown, @@ -421,7 +420,9 @@ def changeToMainWindow(self): self.view.hide() mainProgram = self.host.host mainProgram.changeFocusTo(mainProgram.inputWindow) - self.callerSemaphore.release() + if self.callerSemaphore: + self.callerSemaphore.release() + self.callerSemaphore = None def setTextBuffer(self, textBuffer): self.textBuffer = textBuffer diff --git a/app/file_stats.py b/app/file_stats.py index 8e389ead..f825c549 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -53,7 +53,8 @@ def run(self): program.popupWindow.setMessage( "The file on disk has changed.\nReload file?") program.popupWindow.controller.callerSemaphore = self.thread.semaphore - app.background.bg.put((program, 'popup', None)) + app.background.bg.put((program, 'popup', self.thread.semaphore)) + self.thread.semaphore.acquire() # Wait for popup to load redraw = True waitOnSemaphore = True # Check if file read permissions have changed. @@ -63,7 +64,7 @@ def run(self): if redraw: # Send a redraw request. app.background.bg.put((program, 'redraw', self.thread.semaphore)) - self.thread.semaphore.acquire() + self.thread.semaphore.acquire() # Wait for redraw to finish if waitOnSemaphore: self.thread.semaphore.acquire() # Wait for user to respond to popup. time.sleep(self.pollingInterval) From 48252e6a95d55cbf9c478f5547ac1a0de00ba5e3 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 4 Jan 2018 10:44:07 -0800 Subject: [PATCH 33/39] Changed color of the popup window and added support for 8 color terminals --- app/default_prefs.py | 2 ++ app/window.py | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/default_prefs.py b/app/default_prefs.py index 96138171..0b428f3e 100644 --- a/app/default_prefs.py +++ b/app/default_prefs.py @@ -132,6 +132,7 @@ def numberTest(str, expectRegs): 'misspelling': 3, 'number': 1, 'outside_document': 7, + 'popup_window': 0, 'pound_comment': 3, 'py_raw_string1': 2, 'py_raw_string2': 2, @@ -186,6 +187,7 @@ def numberTest(str, expectRegs): 'misspelling': 9, 'number': 31, 'outside_document': outsideOfBufferColorIndex, + 'popup_window': 117, 'pound_comment': commentColorIndex, 'py_raw_string1': stringColorIndex, 'py_raw_string2': stringColorIndex, diff --git a/app/window.py b/app/window.py index 0b93cfc0..966695a7 100755 --- a/app/window.py +++ b/app/window.py @@ -1205,20 +1205,19 @@ def render(self): rows = min(len(self.message) + 4, maxRows) self.resizeTo(rows, cols) self.moveTo(maxRows / 2 - rows / 2, maxCols / 2 - cols / 2) + color = app.color.get('popup_window') for row in range(rows): if row == rows - 2 and self.showOptions: message = '/'.join(self.options) elif row == 0 or row >= rows - 3: - self.addStr(row, 0, ' ' * cols, app.color.get(70)) + self.addStr(row, 0, ' ' * cols, color) continue else: message = self.message[row - 1] lineLength = len(message) spacing1 = (cols - lineLength) / 2 spacing2 = cols - lineLength - spacing1 - self.addStr(row, 0, - ' ' * spacing1 + message + ' ' * spacing2, - app.color.get(70)) + self.addStr(row, 0, ' ' * spacing1 + message + ' ' * spacing2, color) def setMessage(self, message): """ From dbce96356a1ea36716d61a03aff506580fbe5ee7 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Fri, 5 Jan 2018 13:21:33 -0800 Subject: [PATCH 34/39] Cleaned up code and fixed a bug that would sometimes appear if tempChange was not none when you try to reload the file --- app/actions.py | 9 +++++---- app/background.py | 13 +++---------- app/curses_util.py | 1 - app/file_stats.py | 16 +++++++++++++--- app/history.py | 3 +-- app/window.py | 2 +- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/actions.py b/app/actions.py index 24da6b45..c4b01bc5 100644 --- a/app/actions.py +++ b/app/actions.py @@ -845,15 +845,16 @@ def restoreUserHistory(self): self.penRow, self.penCol = self.fileHistory.setdefault('pen', (0, 0)) self.view.scrollRow, self.view.scrollCol = self.fileHistory.setdefault( 'scroll', (0, 0)) - self.doSelectionMode(self.fileHistory.setdefault('selectionMode', - app.selectable.kSelectionNone)) - self.markerRow, self.markerCol = self.fileHistory.setdefault('marker', - (0, 0)) if app.prefs.editor['saveUndo']: self.redoChain = self.fileHistory.setdefault('redoChainCompound', []) self.savedAtRedoIndex = self.fileHistory.setdefault('savedAtRedoIndexCompound', 0) self.redoIndex = self.savedAtRedoIndex self.oldRedoIndex = self.savedAtRedoIndex + self.tempChange = None + self.doSelectionMode(self.fileHistory.setdefault('selectionMode', + app.selectable.kSelectionNone)) + self.markerRow, self.markerCol = self.fileHistory.setdefault('marker', + (0, 0)) # Restore file bookmarks self.bookmarks = self.fileHistory.setdefault('bookmarks', []) diff --git a/app/background.py b/app/background.py index 751767b4..34cab53e 100644 --- a/app/background.py +++ b/app/background.py @@ -46,6 +46,8 @@ def put(self, data): def background(inputQueue, outputQueue): + pid = os.getpid() + signalNumber = signal.SIGUSR1 def redrawProgram(program, callerSemaphore): """ Sends a SIGUSR1 signal to the current program and draws its screen. @@ -56,8 +58,6 @@ def redrawProgram(program, callerSemaphore): Returns: None. """ - pid = os.getpid() - signalNumber = signal.SIGUSR1 program.render() outputQueue.put((app.render.frame.grabFrame(), callerSemaphore)) os.kill(pid, signalNumber) @@ -71,16 +71,9 @@ def redrawProgram(program, callerSemaphore): if message == 'quit': app.log.info('bg received quit message') return - elif message == 'redraw': - app.log.info('bg received redraw message') - assert(callerSemaphore != None) - redrawProgram(program, callerSemaphore) - continue elif message == 'popup': app.log.meta('bg received popup message') # assert(callerSemaphore != None) - pid = os.getpid() - signalNumber = signal.SIGUSR1 outputQueue.put((('popup', None), callerSemaphore)) os.kill(pid, signalNumber) continue @@ -105,7 +98,7 @@ def redrawProgram(program, callerSemaphore): errorType, value, tracebackInfo = sys.exc_info() out = traceback.format_exception(errorType, value, tracebackInfo) outputQueue.put((('exception', out), None)) - os.kill(os.getpid(), signal.SIGUSR1) + os.kill(pid, signalNumber) while True: program, message, callerSemaphore = inputQueue.get() if message == 'quit': diff --git a/app/curses_util.py b/app/curses_util.py index b7280d6c..f011491d 100644 --- a/app/curses_util.py +++ b/app/curses_util.py @@ -218,6 +218,5 @@ def windowChangedHandler(signum, frame): curses.ungetch(curses.KEY_RESIZE) signal.signal(signal.SIGWINCH, windowChangedHandler) def wakeGetch(signum, frame): - import app.log curses.ungetch(0) signal.signal(signal.SIGUSR1, wakeGetch) diff --git a/app/file_stats.py b/app/file_stats.py index f825c549..b26c0016 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -32,9 +32,12 @@ def __init__(self, fullPath='', pollingInterval=2): # the most updated version of the file's stats. self.fileStats = None self.pollingInterval = pollingInterval + # This is updated only when self.fileInfo has been fully updated. We use + # this variable in order to not have to wait for the statsLock. + self.currentFileInfo = {'isReadOnly': False, + 'size': 0} # All necessary file info should be placed in this dictionary. - self.fileInfo = {'isReadOnly': False, - 'size': 0} + self.fileInfo = self.currentFileInfo.copy() # Used to determine if file on disk has changed since the last save self.savedFileStat = None self.statsLock = threading.Lock() @@ -63,7 +66,7 @@ def run(self): redraw = True if redraw: # Send a redraw request. - app.background.bg.put((program, 'redraw', self.thread.semaphore)) + app.background.bg.put((program, [], self.thread.semaphore)) self.thread.semaphore.acquire() # Wait for redraw to finish if waitOnSemaphore: self.thread.semaphore.acquire() # Wait for user to respond to popup. @@ -101,6 +104,7 @@ def updateStats(self): self.fileStats = os.stat(self.fullPath) self.fileInfo['isReadOnly'] = not os.access(self.fullPath, os.W_OK) self.fileInfo['size'] = self.fileStats.st_size + self.currentFileInfo = self.fileInfo.copy() self.statsLock.release() return True except Exception as e: @@ -119,6 +123,12 @@ def getUpdatedFileInfo(self): self.statsLock.release() return info + def getCurrentFileInfo(self): + """ + Retrieves the current file info that we have in memory. + """ + return self.currentFileInfo + def setTextBuffer(self, textBuffer): self.textBuffer = textBuffer diff --git a/app/history.py b/app/history.py index a5e9ca48..76509efc 100644 --- a/app/history.py +++ b/app/history.py @@ -87,9 +87,8 @@ def getFileInfo(fileStats, data=None): Returns: A tuple containing the (checksum, fileSize) of the file. """ - fileInfo = fileStats.getUpdatedFileInfo() + fileSize = fileStats.getUpdatedFileInfo()['size'] checksum = calculateChecksum(fileStats.fullPath, data) - fileSize = fileInfo['size'] return (checksum, fileSize) def getFileHistory(fileStats, data=None): diff --git a/app/window.py b/app/window.py index 966695a7..60d4a02d 100755 --- a/app/window.py +++ b/app/window.py @@ -627,7 +627,7 @@ def onChange(self): lineCursor -= 1 pathLine = self.host.textBuffer.fullPath if 1: - if tb.fileStats.getUpdatedFileInfo()['isReadOnly']: + if tb.fileStats.getCurrentFileInfo()['isReadOnly']: pathLine += ' [RO]' if 1: if tb.isDirty(): From 447483153a0b0bdd81574689b925ce8b1d76f4f5 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 11 Jan 2018 09:25:35 -0800 Subject: [PATCH 35/39] Overrode setTextBuffer method so that it tells controller when the popup's text buffer has changed. --- app/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/window.py b/app/window.py index 60d4a02d..503c4e15 100755 --- a/app/window.py +++ b/app/window.py @@ -1190,7 +1190,6 @@ def __init__(self, host): self.host = host self.controller = app.cu_editor.PopupController(self) self.setTextBuffer(app.text_buffer.TextBuffer()) - self.controller.setTextBuffer(self.textBuffer) self.longestLineLength = 0 self.message = [] self.showOptions = True From c8de77f4022c34bcd2fd35b6d7e5effcc92ad87f Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 18 Jan 2018 11:01:50 -0800 Subject: [PATCH 36/39] Added copyright comment to file_stats.py --- app/file_stats.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/file_stats.py b/app/file_stats.py index b26c0016..e9368133 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -1,3 +1,17 @@ +# Copyright 2018 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import app.background import app.log import app.prefs From 0bcfd0c1ccd56126a740ceb0a45644361bf25640 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Thu, 18 Jan 2018 11:11:23 -0800 Subject: [PATCH 37/39] Merge with updated master --- app/window.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/window.py b/app/window.py index 503c4e15..f83de7f6 100755 --- a/app/window.py +++ b/app/window.py @@ -1255,3 +1255,11 @@ def render(self): for k in range(rows): self.addStr(k, i * 5, ' %3d ' % (i + k * width,), app.color.get(i + k * width)) + + def setTextBuffer(self, textBuffer): + Window.setTextBuffer(self, textBuffer) + self.controller.setTextBuffer(textBuffer) + + def unfocus(self): + self.hide() + Window.unfocus(self) From cf38871ea923a55768c54740ab23b72b06065e1e Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Mon, 22 Jan 2018 11:30:13 -0800 Subject: [PATCH 38/39] Readd deleted line which fixes parsing issues and rebased to master --- app/background.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/background.py b/app/background.py index 34cab53e..20a9cf84 100644 --- a/app/background.py +++ b/app/background.py @@ -78,6 +78,7 @@ def redrawProgram(program, callerSemaphore): os.kill(pid, signalNumber) continue program.executeCommandList(message) + program.focusedWindow.textBuffer.parseScreenMaybe() redrawProgram(program, callerSemaphore) #app.profile.endPythonProfile(profile) if not inputQueue.empty(): From 4ec9d8ed10ff1338fedb42dc7da0e3f8371e4b22 Mon Sep 17 00:00:00 2001 From: Aaron Xu Date: Sun, 4 Feb 2018 15:45:03 -0800 Subject: [PATCH 39/39] Fixed the fileStats thread to work with the new popup window from master. Changed a lot of the API for the popup window to make it more thread safe --- app/buffer_manager.py | 1 + app/ci_program.py | 5 ++- app/cu_editor.py | 18 ++--------- app/file_stats.py | 35 ++++++++++++++++++--- app/window.py | 73 ++++++++++++++++++++++++++++++++++++------- 5 files changed, 97 insertions(+), 35 deletions(-) diff --git a/app/buffer_manager.py b/app/buffer_manager.py index 9f4487d8..0b4820db 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -93,6 +93,7 @@ def loadTextBuffer(self, relPath, view): textBuffer = app.text_buffer.TextBuffer() textBuffer.view = view self.renameBuffer(textBuffer, fullPath) + textBuffer.fileStats.setPopupWindow(view.popupWindow) textBuffer.fileLoad() self.buffers.append(textBuffer) if 0: diff --git a/app/ci_program.py b/app/ci_program.py index 3c33490b..ac71e4c4 100755 --- a/app/ci_program.py +++ b/app/ci_program.py @@ -131,7 +131,7 @@ def commandLoop(self): self.exiting = True return if frame[0] == 'popup': - self.changeFocusTo(self.popupWindow) + self.changeFocusTo(self.inputWindow.popupWindow) callerSemaphore.release() else: self.refresh(frame[0], frame[1]) @@ -224,7 +224,7 @@ def commandLoop(self): self.exiting = True return if frame[0] == 'popup': - self.changeFocusTo(self.popupWindow) + self.changeFocusTo(self.inputWindow.popupWindow) callerSemaphore.release() else: self.refresh(frame[0], frame[1]) @@ -295,7 +295,6 @@ def startup(self): self.debugWindow = None self.debugUndoWindow = None self.logWindow = None - self.popupWindow = app.window.PopupWindow(self) self.paletteWindow = app.window.PaletteWindow(self) self.inputWindow = app.window.InputWindow(self) self.zOrder.append(self.inputWindow) diff --git a/app/cu_editor.py b/app/cu_editor.py index 34811c9c..b68ea73b 100644 --- a/app/cu_editor.py +++ b/app/cu_editor.py @@ -434,23 +434,9 @@ def reloadBuffer(self): TODO: Make this reloading a new change that can be appended to the redo chain so user can undo out of a reloadBuffer call. """ - mainBuffer = self.view.host.inputWindow.textBuffer + mainBuffer = self.view.host.textBuffer mainBuffer.fileLoad() - self.changeToMainWindow() - - def setOptions(self, options): - """ - This function is used to change the options that are displayed in the - popup window as well as their functions. - - Args: - options (dict): A dictionary mapping keys (ints) to its - corresponding action. - - Returns; - None. - """ - self.commandSet = options + self.changeToInputWindow() class PaletteDialogController(app.controller.Controller): """.""" diff --git a/app/file_stats.py b/app/file_stats.py index e9368133..3252befd 100644 --- a/app/file_stats.py +++ b/app/file_stats.py @@ -13,6 +13,7 @@ # limitations under the License. import app.background +import app.curses_util import app.log import app.prefs import os @@ -20,7 +21,6 @@ import threading import app.window - class FileTracker(threading.Thread): def __init__(self, *args, **keywords): threading.Thread.__init__(self, *args, **keywords) @@ -66,10 +66,12 @@ def run(self): redraw = False waitOnSemaphore = False if program: - if self.fileContentChangedSinceCheck(): - program.popupWindow.setMessage( - "The file on disk has changed.\nReload file?") - program.popupWindow.controller.callerSemaphore = self.thread.semaphore + if self.fileContentChangedSinceCheck() and self.__popupWindow: + self.__popupWindow.setUpWindow( + message="The file on disk has changed.\nReload file?", + displayOptions=self.__popupDisplayOptions, + controllerOptions=self.__popupControllerOptions) + self.__popupWindow.controller.callerSemaphore = self.thread.semaphore app.background.bg.put((program, 'popup', self.thread.semaphore)) self.thread.semaphore.acquire() # Wait for popup to load redraw = True @@ -143,6 +145,29 @@ def getCurrentFileInfo(self): """ return self.currentFileInfo + def setPopupWindow(self, popupWindow): + """ + Sets the file stat's object's reference to the popup window that + it will use to notify the user of any changes. + + Args: + popupWindow (PopupWindow): The popup window that this object will use. + + Returns: + None. + """ + # The keys that the user can press to respond to the popup window. + self.__popupControllerOptions = { + ord('Y'): popupWindow.controller.reloadBuffer, + ord('y'): popupWindow.controller.reloadBuffer, + ord('N'): popupWindow.controller.changeToInputWindow, + ord('n'): popupWindow.controller.changeToInputWindow, + app.curses_util.KEY_ESCAPE: popupWindow.controller.changeToInputWindow, + } + # The options that will be displayed on the popup window. + self.__popupDisplayOptions = ['Y', 'N'] + self.__popupWindow = popupWindow + def setTextBuffer(self, textBuffer): self.textBuffer = textBuffer diff --git a/app/window.py b/app/window.py index 63a3eb37..a0d73ed4 100755 --- a/app/window.py +++ b/app/window.py @@ -24,6 +24,7 @@ import app.text_buffer import app.vi_editor import bisect +import threading import os import sys import curses @@ -1193,26 +1194,30 @@ def __init__(self, host): self.host = host self.controller = app.cu_editor.PopupController(self) self.setTextBuffer(app.text_buffer.TextBuffer()) - self.longestLineLength = 0 self.__message = [] self.showOptions = True # This will be displayed and should contain the keys that respond to user # input. This should be updated if you change the controller's command set. - self.options = [] + self.__displayOptions = [] + # Prevent sync issues from occuring when setting options. + self.__optionsLock = threading.Lock() def render(self): """ Display a box of text in the center of the window. """ maxRows, maxCols = self.host.rows, self.host.cols - cols = min(self.longestLineLength + 6, maxCols) + longestLineLength = 0 + if len(self.__message): + longestLineLength = len(max(self.__message, key=len)) + cols = min(longestLineLength + 6, maxCols) rows = min(len(self.__message) + 4, maxRows) self.resizeTo(rows, cols) self.moveTo(maxRows / 2 - rows / 2, maxCols / 2 - cols / 2) color = app.color.get('popup_window') for row in range(rows): if row == rows - 2 and self.showOptions: - message = '/'.join(self.options) + message = '/'.join(self.__displayOptions) elif row == 0 or row >= rows - 3: self.addStr(row, 0, ' ' * cols, color) continue @@ -1223,27 +1228,73 @@ def render(self): spacing2 = cols - lineLength - spacing1 self.addStr(row, 0, ' ' * spacing1 + message + ' ' * spacing2, color) - def setMessage(self, message): + def __setMessage(self, message): """ Sets the Popup window's message to the given message. - message (str): A string that you want to display. + This function should only be called by setUpWindow. + + Args: + message (str): A string that you want to display. Returns: None. """ self.__message = message.split("\n") - self.longestLineLength = max([len(line) for line in self.__message]) - def setOptionsToDisplay(self, options): + def __setDisplayOptions(self, displayOptions): """ This function is used to change the options that are displayed in the popup window. They will be separated by a '/' character when displayed. + This function should only be called by setUpWindow. + + Args: + displayOptions (list): A list of possible keys which the user can press + and should be responded to by the controller. + """ + self.__displayOptions = displayOptions + + def __setControllerOptions(self, controllerOptions): + """ + This function is used to change the options that are displayed in the + popup window as well as their functions. This function should only be + called by setUpWindow. + + Args: + controllerOptions (dict): A dictionary mapping keys (ints) to its + corresponding action. + + Returns; + None. + """ + self.controller.commandSet = controllerOptions + + def setUpWindow(self, message=None, displayOptions=None, + controllerOptions=None): + """ + Sets up the popup window. You should pass in the following arguments in + case of any sync issues, even if the values seem to already be set. By + default, the values will be set to None, meaning that the values will + not be changed. However, if you wish to set each value to be empty, + message should be an empty string, displayOptions should be an empty list, + and controllerOptions should be an empty dictionary. Args: - options (list): A list of possible keys which the user can press and - should be responded to by the controller. + message (str): The string that you want the popup window to display. + displayOptions (list): The list of strings representing the options + that will be displayed on the popup window. + controllerOptions (dict): The mapping of user keypresses to functions. + + Returns: + None. """ - self.options = options + self.__optionsLock.acquire() + if message is not None: + self.__setMessage(message) + if displayOptions is not None: + self.__setDisplayOptions(displayOptions) + if controllerOptions is not None: + self.__setControllerOptions(controllerOptions) + self.__optionsLock.release() def setTextBuffer(self, textBuffer): Window.setTextBuffer(self, textBuffer)