#JES- Jython Environment for Students #Copyright (C) 2002 Jason Ergle, Claire Bailey, David Raines, Joshua Sklare #See JESCopyright.txt for full licensing information #Revisions: # 5/14/03: added removeErrorHighlighting() to be called before any changes take # place in the text - AdamW # 5/15/03: added call to removeErrorHighlighting before setting error highlighting # to prevent multiple highlightings which can't be undone. -AdamW # 5/15/03: added comment and string highlighting. - AdamW import JESConstants import java.awt as awt import javax.swing as swing import javax.swing.text.DefaultStyledDocument as DefaultStyledDocument import keyword WORD_BREAKS = [' ', '\n', '\t', '[', ']', '{', '}', ',', '\'', '-', '+', '=', '<', '>', ':', ';', '_', '(', ')', '.', '#', '"', '%' ] KEYWORD_BOLD = 1 INSERT_EVENT = 1 REMOVE_EVENT = 2 MAX_UNDO_EVENTS_TO_RETAIN = 500 ERROR_LINE_FONT_COLOR = awt.Color.black ERROR_LINE_BACKGROUND_COLOR = awt.Color.yellow class JESEditorDocument(DefaultStyledDocument): ################################################################################ # Function name: __init__ # Parameters: # -editor: JESEditor object that this object is associated with # Return: # An instance of the JESEditorDocument class. # Description: # Creates an instance of the JESEditorDocument class. ################################################################################ def __init__(self, editor): self.editor = editor self.textAttrib = swing.text.SimpleAttributeSet() self.keywordAttrib = swing.text.SimpleAttributeSet() self.jesEnvironmentWordAttrib = swing.text.SimpleAttributeSet() self.errorLineAttrib = swing.text.SimpleAttributeSet() self.commentAttrib = swing.text.SimpleAttributeSet() self.stringAttrib = swing.text.SimpleAttributeSet() swing.text.StyleConstants.setForeground(self.stringAttrib, JESConstants.STRING_COLOR) swing.text.StyleConstants.setForeground(self.commentAttrib, JESConstants.COMMENT_COLOR) swing.text.StyleConstants.setForeground(self.jesEnvironmentWordAttrib, JESConstants.ENVIRONMENT_WORD_COLOR) swing.text.StyleConstants.setBold(self.jesEnvironmentWordAttrib, KEYWORD_BOLD) swing.text.StyleConstants.setFontSize(self.jesEnvironmentWordAttrib, self.editor.program.userFont) swing.text.StyleConstants.setFontSize(self.textAttrib, self.editor.program.userFont) swing.text.StyleConstants.setBackground(self.textAttrib, awt.Color.white) swing.text.StyleConstants.setForeground(self.keywordAttrib, JESConstants.KEYWORD_COLOR) swing.text.StyleConstants.setBold(self.keywordAttrib, KEYWORD_BOLD) swing.text.StyleConstants.setFontSize(self.keywordAttrib, self.editor.program.userFont) swing.text.StyleConstants.setForeground(self.errorLineAttrib, ERROR_LINE_FONT_COLOR) swing.text.StyleConstants.setBackground(self.errorLineAttrib, ERROR_LINE_BACKGROUND_COLOR) swing.text.StyleConstants.setFontSize(self.errorLineAttrib, self.editor.program.userFont) self.undoEvents = [] #The following variables are set when showErrorLine is called. They #are then used to unhighlight the line when the next text modification #is made. self.errorLineStart = -1 self.errorLineLen = -1 ################################################################################ # Function name: insertString # Parameters: # -offset: offset where the text will be inserted # -str: string that is being inserted # -a: attribute for the text that is being innserted. # Description: # This function overrides the inherited insertString function. It inserts # the target text and then calls the keywordHighlightEvent function to # highlight keywords. ################################################################################ def insertString(self, offset, str, a, addUndoEvent=1): lineUpdateNeeded = 0 if self.errorLineStart >= 0: self.removeErrorHighlighting() if str == '\t': str = JESConstants.TAB self.editor.modified = 1 self.editor.gui.loadButton.enabled = 1 for char in str: if (char == '#') or (char == '"') or (char == '"'): lineUpdateNeeded = 1 if addUndoEvent: self.addUndoEvent(INSERT_EVENT, offset, str) DefaultStyledDocument.insertString(self, offset, str, self.textAttrib) if lineUpdateNeeded: self.updateLineHighlighting(offset) self.keywordHighlightEvent(offset, len(str)) ################################################################################ # Function name: remove # Parameters: # -offset: offset of the text that is being removed # -len: length of the text that is being removed # Description: # This function overrides the inherited remove function. It removes the # target text and then calls the keywordHighlightEvent function to highlight # keywords. ################################################################################ def remove(self, offset, len, addUndoEvent=1): lineUpdateNeeded = 0 if self.errorLineStart >= 0: self.removeErrorHighlighting() self.editor.modified = 1 self.editor.gui.loadButton.enabled = 1 for char in self.getText(offset, len): if (char == '#') or (char == '"') or (char == '"'): lineUpdateNeeded = 1 if addUndoEvent: self.addUndoEvent(REMOVE_EVENT, offset, self.getText(offset, len)) DefaultStyledDocument.remove(self, offset, len) if lineUpdateNeeded: self.updateLineHighlighting(offset) else: self.keywordHighlightEvent(offset, len) ################################################################################ # Function name: removeErrorHighlighting # Description: # This funciton will remove any error highlighting set between # errorLineStart and errorLineLen. ################################################################################ def removeErrorHighlighting(self): #Unhighlight a line if showErrorLine was called earlier if self.errorLineStart >= 0: self.updateKeywordHighlightInRange(self.errorLineStart, self.errorLineLen) self.errorLineStart = -1 self.errorLineLen = -1 ################################################################################ # Function name: keywordHighlightEvent # Parameters: # -modifiedTextOffset: # -modifiedTextLen: # Description: # This function is called when text is either inserted or removed from the # document. It takes in the text that was modified, and then calculates # where the first word before that text begins as well as where the next # word after that text ends. This can then be used when calling the # updateKeywordHighlightInRange function to let it know where to update # keyword highlights. ################################################################################ def keywordHighlightEvent(self, modifiedTextOffset, modifiedTextLen): #Unhighlight a line if showErrorLine was called earlier if self.errorLineStart >= 0: self.updateKeywordHighlightInRange(self.errorLineStart, self.errorLineLen) self.errorLineStart = -1 self.errorLineLen = -1 #Find start of the word before the modified text startOffset = modifiedTextOffset - 1 while startOffset > 0: startOffset -= 1 curChar = self.getText(startOffset, 1) if curChar in WORD_BREAKS: break #Find end of the word after the modified text endOffset = modifiedTextOffset + modifiedTextLen while endOffset <= self.length: curChar = self.getText(endOffset, 1) if curChar in WORD_BREAKS: break endOffset += 1 #Ensure that the start and end offsets are within vaild document range if endOffset >= self.length: endOffset = self.length if startOffset < 0: startOffset = 0 self.updateKeywordHighlightInRange(startOffset, endOffset - startOffset) ################################################################################ # Function name: updateKeywordHighlightInRange # Parameters: # -offset: offset where text begins that needs to be checked for keywords # -len: length of the text which needs to be checked for keywords # Description: # This function takes in a text offset and length, searches for keywords in # the text, and highlights those keywords. # It does this by first removing the entire text and replacing it with # default attribute text. This ensures that any text that once was a # keyword will not be highlighted anymore. Then the text is searched for # keywords; when a keyword is found, that text is replaced with the keyword # highlighted text. ################################################################################ def updateKeywordHighlightInRange(self, offset, len): #Save the cursor position so it can be reset later try: curOffset = self.editor.getCaretPosition() rangeText = self.getText(offset, len) #Delete all text within range and replace with default style text DefaultStyledDocument.remove(self, offset, len) DefaultStyledDocument.insertString(self, offset, rangeText, self.textAttrib) i = 0 iWordStart = 0 #Read each character in the range until an entire word is found fun = self.setTextAttrib r = rangeText[:len] for curChar in r: #If the currect char is a word break char, then an entire word has #been read if curChar in WORD_BREAKS: #Make sure to comment color word breaks if they are in comments #blocks (AW 5/15/03) if self.isComment(offset + i): DefaultStyledDocument.remove(self, offset + i, 1) DefaultStyledDocument.insertString(self, offset + i, curChar, self.commentAttrib) #Make sure to color strings: (AW 5/15/03) elif self.isString(offset + i): DefaultStyledDocument.remove(self, offset + i, 1) DefaultStyledDocument.insertString(self, offset + i, curChar, self.stringAttrib) self.setTextAttrib(offset + iWordStart, rangeText[iWordStart:i]) iWordStart = i + 1 else: pass i += 1 self.setTextAttrib(offset + iWordStart, rangeText[iWordStart:i]) self.editor.setCaretPosition(curOffset) except: pass ################################################################################ # Function name: setTextAttrib # Parameters: # -offset: offset where text begins the will be updated # -text: text that will be updated # Description: # This function is called set the text attribute of the specified text. It # will check to see if that text is a keyword, and if so, will set the # text attribute so that the word has the correct keyword highlighting. ################################################################################ def setTextAttrib(self, offset, text): if self.isComment(offset): DefaultStyledDocument.remove(self, offset, len(text)) DefaultStyledDocument.insertString(self, offset, text, self.commentAttrib) elif self.isString(offset): DefaultStyledDocument.remove(self, offset, len(text)) DefaultStyledDocument.insertString(self, offset, text, self.stringAttrib) else: if keyword.iskeyword(text): DefaultStyledDocument.remove(self, offset, len(text)) DefaultStyledDocument.insertString(self, offset, text, self.keywordAttrib) if self.isJESEnvironmentWord(text): DefaultStyledDocument.remove(self, offset, len(text)) DefaultStyledDocument.insertString(self, offset, text, self.jesEnvironmentWordAttrib) def isJESEnvironmentWord(self,text): varsToHighlight = self.editor.program.getVarsToHighlight() if varsToHighlight.has_key(text): return not None return None ################################################################################ # Funciton name: updateLineHighlighting # Parameters: # -offset: the location in the text which has been changed # Description: # This should be called whenever a ', ", or # is added or removed from the # text. It updates the string and comment highlighting to match the current # formatting. ################################################################################ def updateLineHighlighting(self, offset): text = self.getText(0, self.getLength()) linestart = text.rfind("\n", 0, offset) if linestart == -1: linestart = 0 lineend = text.find("\n", offset) if lineend == -1: lineend = self.getLength() self.updateKeywordHighlightInRange(linestart, (lineend - linestart)) ################################################################################ # Function name: isComment # Parameters: # -offset: the location of the text to see if it's a comment(#) # Description: # Takes in a location of text, and then examines the line that it's on to # determine if the location is inside a comment. Will also find out if # the comment is inside a string and not color it. ################################################################################ def isComment(self, offset): isSingleString = 0 isDoubleString = 0 text = self.getText(0, self.getLength()) linestart = text.rfind("\n", 0, offset) if linestart == -1: linestart = 0 for loc in range(linestart, offset + 1): if text[loc] == '"': if (isDoubleString == 1) and (isSingleString == 0): isDoubleString = 0 elif (isDoubleString == 0) and (isSingleString == 0): isDoubleString = 1 if text[loc] == "'": if (isSingleString == 1) and (isDoubleString == 0): isSingleString = 0 elif (isSingleString == 0) and (isDoubleString == 0): isSingleString = 1 if text[loc] == '#': if (isSingleString == 0) and (isDoubleString == 0): return 1 return 0 ################################################################################ # Function name: isString # Parameters: # -offset: the location of the text to see if it's a string # Description: # Takes in a location, and determines if that location is inside a string ################################################################################ def isString(self, offset): isSingleString = 0 isDoubleString = 0 nextDOff = 0 nextSOff = 0 text = self.getText(0, self.getLength()) linestart = text.rfind("\n", 0, offset) if linestart == -1: linestart = 0 for loc in range(linestart, offset + 1): if nextDOff: isDoubleString = 0 nextDOff = 0 if nextSOff: isSingleString = 0 nextSOff = 0 if text[loc] == '"': if (isDoubleString == 0) and (isSingleString == 0): isDoubleString = 1 elif (isDoubleString == 1) and (isSingleString == 0): nextDOff = 1 if text[loc] == "'": if (isSingleString == 0) and (isDoubleString == 0): isSingleString = 1 elif (isSingleString == 1) and (isDoubleString == 0): nextSOff = 1 return (isSingleString or isDoubleString) ################################################################################ # Function name: addUndoEvent # Parameters: # -eventType: identifies the type of event that occured (insert or remove) # -offset: offset in the text that the event occured in # -str: text that is being inserted or removed # Description: # Adds an undo event to the event array. This is needed so that text # modification events can be undone. If the array reaches it's maximum # capacity, then the oldest event is removed from the array before adding # the new one. ################################################################################ def addUndoEvent(self, eventType, offset, str): if len(self.undoEvents) > MAX_UNDO_EVENTS_TO_RETAIN: del self.undoEvents[0] self.undoEvents.append([eventType, offset, str]) ################################################################################ # Function name: undo # Description: # Undoes the last text modification that is in the undo events array and # removes it from the array. ################################################################################ def undo(self): if len(self.undoEvents) > 0: lastEvent = self.undoEvents.pop() if lastEvent[0] == INSERT_EVENT: self.remove(lastEvent[1], len(lastEvent[2]), 0) else: self.insertString(lastEvent[1], lastEvent[2], self.textAttrib, 0) ################################################################################ # Function name: showErrorLine # Parameters: # -lineNumber: number of the line that should be highlighted # Description: # When this function is called, the specified line will be highlighted so # that the user can tell which line contains an error. ################################################################################ def showErrorLine(self, lineNumber): #remove any old error highlighting, because we only want to show one error # at a time. Plus, the system only keeps track of one error, so we need to # unhighlight the old error before setting the new one (AW 5/15/03) if self.errorLineStart > 0: self.removeErrorHighlighting() #Search for the start offset of the error line docText = self.getText(0, self.getLength()) line = 1 offset = 0 while line < lineNumber: offset = docText.find('\n', offset) + 1 line += 1 #Search for the end offset of the error line #offset += 1 endOffset = docText.find('\n', offset) if endOffset == -1: endOffset = len(docText) #Set error line position and length object variables self.errorLineStart = offset self.errorLineLen = endOffset - offset #Set the correct text attribute for the error line self.setCharacterAttributes(self.errorLineStart, self.errorLineLen, self.errorLineAttrib, 0) #Set cusor to error line to ensure that the error line will be visible self.editor.setCaretPosition(self.errorLineStart) ################################################################################ # Function name: gotoLine # Parameters: # -lineNumber: number of the line that should be highlighted # Description: # When this function is called, the specified line will be highlighted so # that the user can tell which line contains an error. ################################################################################ def gotoLine(self, lineNumber): #Search for the start offset of the error line docText = self.getText(0, self.getLength()) line = 1 offset = 0 while line < lineNumber: offset = docText.find('\n', offset) + 1 line += 1 #Search for the end offset of the target line #offset += 1 endOffset = docText.find('\n', offset) if endOffset == -1: endOffset = len(docText) #Set target line position and length object variables self.targetLineStart = offset self.targetLineLen = endOffset - offset #Set cusor to target line to ensure that the error line will be visible self.editor.setCaretPosition(self.targetLineStart) def searchForward(self,toFind): try: offset = self.editor.getCaretPosition() text=self.getText(offset,self.getLength()-offset) location=text.find(toFind) if location != -1: #Highlight Text self.setCharacterAttributes(0, self.getLength(), self.textAttrib, 0) self.setCharacterAttributes(location+offset, len(toFind), self.errorLineAttrib, 0) self.editor.setCaretPosition(location+offset+len(toFind)) else: if self.editor.getCaretPosition()>1: self.editor.setCaretPosition(1) self.searchForward(toFind) except: print "Exception thrown in searchForward" import sys a,b,c=sys.exc_info() print a,b,c def searchBackward(self,toFind): try: offset = self.editor.getCaretPosition() text=self.getText(0,offset) location=text.rfind(toFind) if location != -1: #Unhighlight Text self.setCharacterAttributes(0, self.getLength(), self.textAttrib, 0) #Highlight Text self.setCharacterAttributes(location, len(toFind), self.errorLineAttrib, 0) self.editor.setCaretPosition(location) else: if self.editor.getCaretPosition()