diff --git a/app/actions.py b/app/actions.py index 333d26eb..3ac0d9fa 100644 --- a/app/actions.py +++ b/app/actions.py @@ -773,8 +773,6 @@ def fileFilter(self, data): 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: @@ -797,7 +795,7 @@ 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.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()) @@ -833,7 +831,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( @@ -841,22 +839,23 @@ 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', []) # Store the file's info. self.lastChecksum, self.lastFileSize = app.history.getFileInfo( - self.fullPath) + self.fileStats) def updateBasicScrollPosition(self): """ @@ -953,7 +952,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']: @@ -980,20 +978,17 @@ 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.fileStat = os.stat(self.fullPath) - # If we're writing this file for the first time, self.isReadOnly will - # still be True (from when it didn't exist). - self.isReadOnly = False + self.fileStats) + self.fileStats.savedFileStat = self.fileStats.fileStats self.setMessage('File saved') except Exception as e: color = app.color.get('status_line_error') - if self.isReadOnly: - self.setMessage("Permission error. Try modifying in sudo mode.", + if self.fileStats.getUpdatedFileInfo()['isReadOnly']: + self.setMessage("Permission error. Try modifing in sudo mode.", color=color) else: self.setMessage( diff --git a/app/background.py b/app/background.py index abda0170..bc68872b 100644 --- a/app/background.py +++ b/app/background.py @@ -47,22 +47,40 @@ def put(self, data): def background(inputQueue, outputQueue): - block = True pid = os.getpid() signalNumber = signal.SIGUSR1 + def redrawProgram(program, callerSemaphore): + """ + Sends a SIGUSR1 signal to the current program and draws its screen. + + Args: + program (CiProgram): an instance of the CiProgram object. + + Returns: + None. + """ + program.render() + outputQueue.put((app.render.frame.grabFrame(), callerSemaphore)) + os.kill(pid, signalNumber) + + block = True while True: try: try: - program, message = 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 == 'popup': + app.log.meta('bg received popup message') + # assert(callerSemaphore != None) + outputQueue.put((('popup', None), callerSemaphore)) + os.kill(pid, signalNumber) + continue program.executeCommandList(message) program.focusedWindow.textBuffer.parseScreenMaybe() - program.render() - outputQueue.put(app.render.frame.grabFrame()) - os.kill(pid, signalNumber) + redrawProgram(program, callerSemaphore) #app.profile.endPythonProfile(profile) if not inputQueue.empty(): continue @@ -75,18 +93,16 @@ 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, 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(pid, signalNumber) 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/buffer_manager.py b/app/buffer_manager.py index 8cd06033..56b71e34 100644 --- a/app/buffer_manager.py +++ b/app/buffer_manager.py @@ -105,6 +105,8 @@ def loadTextBuffer(self, relPath, view): textBuffer = app.text_buffer.TextBuffer() textBuffer.setFilePath(fullPath) textBuffer.view = view + self.renameBuffer(textBuffer, fullPath) + textBuffer.fileStats.setPopupWindow(view.popupWindow) textBuffer.fileLoad() self.buffers.append(textBuffer) if 0: @@ -142,6 +144,14 @@ def untrackBuffer_(self, fileBuffer): app.log.debug(fileBuffer.fullPath) 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. + """ + fileBuffer.fullPath = fullPath + fileBuffer.changeFileStats() # Track this new file. + def fileClose(self, path): pass diff --git a/app/ci_program.py b/app/ci_program.py index c426a446..d6da0074 100755 --- a/app/ci_program.py +++ b/app/ci_program.py @@ -102,7 +102,7 @@ def commandLoop(self): start = time.time() # The first render, to get something on the screen. if useBgThread: - self.bg.put((self.programWindow, [])) + self.bg.put((self.programWindow, [], None)) else: self.render() # This is the 'main loop'. Execution doesn't leave this loop until the @@ -110,13 +110,20 @@ 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]) self.exiting = True return - self.refresh(frame[0], frame[1]) + if frame[0] == 'popup': + self.programWindow.changeFocusTo( + self.programWindow.inputWindow.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]) @@ -197,16 +204,21 @@ 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() + frame, callerSemaphore = self.bg.get() if frame[0] == 'exception': for line in frame[1]: userMessage(line[:-1]) self.exiting = True return - if frame is not None: - self.refresh(frame[0], frame[1]) + if frame[0] == 'popup': + self.programWindow.changeFocusTo(self.programWindow. + inputWindow.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: @@ -220,7 +232,7 @@ def commandLoop(self): start = time.time() if len(cmdList): if useBgThread: - self.bg.put((self.programWindow, cmdList)) + self.bg.put((self.programWindow, cmdList, None)) else: self.programWindow.executeCommandList(cmdList) self.render() @@ -376,7 +388,7 @@ def run(self): else: self.commandLoop() if app.prefs.editor['useBgThread']: - self.bg.put((self.programWindow, 'quit')) + self.bg.put((self.programWindow, 'quit', None)) self.bg.join() def setUpPalette(self): diff --git a/app/controller.py b/app/controller.py index 0ed3832f..59a7ccca 100644 --- a/app/controller.py +++ b/app/controller.py @@ -73,6 +73,9 @@ def changeToPaletteWindow(self): def changeToPopup(self): self.findAndChangeTo('popupWindow') + def changeToPopup(self): + self.host.changeFocusTo(self.host.host.popupWindow) + def changeToPrediction(self): self.findAndChangeTo('interactivePrediction') diff --git a/app/cu_editor.py b/app/cu_editor.py index bff9e241..d69427b0 100644 --- a/app/cu_editor.py +++ b/app/cu_editor.py @@ -378,23 +378,18 @@ def changeToInputWindow(self): self.callerSemaphore.release() self.callerSemaphore = None - def setOptions(self, options): + def reloadBuffer(self): """ - This function is used to change the options that are displayed in the - popup window as well as their functions. + 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. - Args: - options (dict): A dictionary mapping keys (ints) to its - corresponding action. - - Returns; - None. + TODO: Make this reloading a new change that can be appended + to the redo chain so user can undo out of a reloadBuffer call. """ - self.commandSet = options - - def setTextBuffer(self, textBuffer): - self.textBuffer = textBuffer - + mainBuffer = self.view.host.textBuffer + mainBuffer.fileLoad() + self.changeToInputWindow() class PaletteDialogController(app.controller.Controller): """.""" diff --git a/app/file_stats.py b/app/file_stats.py new file mode 100644 index 00000000..3252befd --- /dev/null +++ b/app/file_stats.py @@ -0,0 +1,250 @@ +# 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.curses_util +import app.log +import app.prefs +import os +import time +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 + 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): + """ + Args: + 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.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 + # 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 = self.currentFileInfo.copy() + # 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 + self.updateStats() + + def run(self): + while not self.thread.shouldExit: + oldFileIsReadOnly = self.fileInfo['isReadOnly'] + program = self.textBuffer.view.host + redraw = False + waitOnSemaphore = False + if program: + 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 + 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, [], 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. + time.sleep(self.pollingInterval) + + def startTracking(self): + """ + 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 (FileTracker object). + """ + 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): + """ + 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: + self.statsLock.acquire() + 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: + app.log.info("Exception occurred while updating file stats thread:", e) + self.statsLock.release() + return False + + def getUpdatedFileInfo(self): + """ + Syncs the in-memory file information with the information on disk. It + then returns the newly updated file information. + """ + self.updateStats() + self.statsLock.acquire() + info = self.fileInfo.copy() # Shallow copy. + self.statsLock.release() + return info + + def getCurrentFileInfo(self): + """ + Retrieves the current file info that we have in memory. + """ + 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 + + 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 + time, file size, and other statistics. + + Args: + None. + + Returns: + True if the file on disk has changed. Otherwise, False. + """ + try: + if (self.updateStats() and self.fileStats): + s1 = self.fileStats + s2 = self.savedFileStat + 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) + 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 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 saved the file. + + 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/history.py b/app/history.py index a96a9a64..c71ee8d2 100644 --- a/app/history.py +++ b/app/history.py @@ -45,12 +45,14 @@ 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). + 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. @@ -59,12 +61,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) userHistory[(newChecksum, newFileSize)] = fileHistory with open(historyPath, 'wb') as file: pickle.dump(userHistory, file) @@ -72,7 +74,25 @@ def saveUserHistory(fileInfo, fileHistory, historyPath=pathToHistory): except Exception as e: app.log.exception(e) -def getFileHistory(filePath, data=None): +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 + returned by calling read() on a file object. + + Returns: + A tuple containing the (checksum, fileSize) of the file. + """ + fileSize = fileStats.getUpdatedFileInfo()['size'] + checksum = calculateChecksum(fileStats.fullPath, data) + return (checksum, fileSize) + +def getFileHistory(fileStats, data=None): """ Takes in an file path and an optimal data argument and checks for the current file's history. @@ -82,36 +102,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. @@ -138,21 +140,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. @@ -170,4 +157,3 @@ def clearUserHistory(): app.log.info("user history cleared") except Exception as e: app.log.error('clearUserHistory exception', e) - diff --git a/app/mutator.py b/app/mutator.py index b8027314..63699b04 100644 --- a/app/mutator.py +++ b/app/mutator.py @@ -16,6 +16,7 @@ import re import app.buffer_file +import app.file_stats import app.log import app.parser import app.prefs @@ -46,9 +47,7 @@ def __init__(self): self.findBackRe = None self.fileExtension = '' self.fullPath = '' - self.fileStat = None self.goalCol = 0 - self.isReadOnly = False self.penGrammar = None self.parser = None self.parserTime = .0 @@ -141,26 +140,7 @@ def isDirty(self): def isSafeToWrite(self): if not os.path.exists(self.fullPath): return True - if self.fileStat is None: - return False - 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) + return not self.fileStats.fileContentChangedSinceSave() def setFilePath(self, path): self.fullPath = app.buffer_file.fullPath(path) diff --git a/app/text_buffer.py b/app/text_buffer.py index 83164e09..59b3bd06 100644 --- a/app/text_buffer.py +++ b/app/text_buffer.py @@ -18,6 +18,7 @@ import app.actions import app.color +import app.file_stats import app.log import app.parser import app.prefs @@ -34,12 +35,31 @@ def __init__(self): self.highlightCursorLine = False self.highlightTrailingWhitespace = True self.fileHistory = {} + self.fileStats = app.file_stats.FileStats(self.fullPath) self.fileEncoding = None self.lastChecksum = None self.lastFileSize = 0 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 diff --git a/app/window.py b/app/window.py index 77f05408..6d5e7e25 100755 --- a/app/window.py +++ b/app/window.py @@ -12,12 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import bisect -import os -import sys -import types -import curses - import app.buffer_manager import app.color import app.config @@ -28,6 +22,13 @@ import app.text_buffer import app.vi_editor +import bisect +import curses +import os +import threading +import types +import sys + # The terminal area that the curses can draw to. mainCursesWindow = None @@ -644,7 +645,7 @@ def onChange(self): lineCursor -= 1 pathLine = self.host.textBuffer.fullPath if 1: - if tb.isReadOnly: + if tb.fileStats.getCurrentFileInfo()['isReadOnly']: pathLine += ' [RO]' if 1: if tb.isDirty(): @@ -1030,26 +1031,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 @@ -1060,26 +1065,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: - options (list): A list of possible keys which the user can press and - should be responded to by the controller. + 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: + 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)