""" This is an editor for Python code with advanced features like double-click selection of groups, command-click copying of chunks of text, function look-up, etc. For more info, see: http://www.strout.net/python/mac/ Last modification: 10/04/99 """ import W from WASTEconst import * import PyEdit import PyFontify import string import Qd import Fm import Evt import Events #-------------------------------------------------------------------------------- # colors... kStringColor = (0, 0x7fff, 0) kCommentColor = (0, 0, 0xb000) kIdentifierColor = (0xbfff, 0, 0) kKeywordColor = (0, 0, 0) # word delimiters... kWordDelims = " .,()[]{}!?@#$%^&*-+/=\r\n\t\"\'\0" # click mappings... # executed with 'self' bound to the PyAdvancedEditor, # 'point' bound to the pixel location clicked, # and 'clickpos' bound to the character position clicked kClickMappings = { Events.cmdKey : "self.InsertChunk(clickpos)", Events.optionKey : "self.HandleQuestion(clickpos)", Events.cmdKey + Events.optionKey : "self.LaunchURL(point)" } #-------------------------------------------------------------------------------- def DragSelectWords(wasteInstance): "While the mouse is down, select any text dragged over in word-sized chunks." origsel = wasteInstance.WEGetSelection() lastsel = sel = origsel while Evt.StillDown(): clickpos = wasteInstance.WEGetOffset(Evt.GetMouse())[0] if clickpos < origsel[0]: # extend selection left to delimiter if chr(wasteInstance.WEGetChar(clickpos)) not in kWordDelims: while chr(wasteInstance.WEGetChar(clickpos-1)) not in kWordDelims: clickpos = clickpos - 1 sel = (clickpos, origsel[1]) elif clickpos > origsel[1]: # extend selection right to delimiter while chr(wasteInstance.WEGetChar(clickpos)) not in kWordDelims: clickpos = clickpos + 1 sel = (origsel[0], clickpos) else: sel = origsel if sel != lastsel: wasteInstance.WESetSelection( sel[0], sel[1] ) lastsel = sel def QuoteBlock(wasteInstance, offset): "Find a block of text enclosed by a quote at the given character offset." charhit = chr(wasteInstance.WEGetChar(offset)) if charhit == "'": targetstate = 1 else: targetstate = 2 # LATER: handle triple quotes! pos = 0 state = 0 # 1:in single quote, 2:in double quote, 3: in triple quote while pos < offset: c = chr(wasteInstance.WEGetChar(pos)) if c == '\\': pos = pos + 1 # skip the character after a backslash! elif c == "'": if state == 1: state = 0 elif state == 0: state = 1 elif c == '"': # LATER: handle triple-quotes if state == 2: state = 0 elif state == 0: state = 2 pos = pos + 1 # now, we know what state we're in ... # if it's 0, search forward; if it's the same state as what # we're blocking on, search back; else it's not a block at all if state == 0: dir = 1 elif state == targetstate: dir = -1 else: return (offset, offset+1) pos = offset+dir while 1: c = chr(wasteInstance.WEGetChar(pos)) if c == '\0': break if c == charhit: if chr(wasteInstance.WEGetChar(pos-1)) != '\\': break pos = pos + dir # found it! if dir > 0: return (offset, pos+1) return (pos, offset+1) def TextBlock(wasteInstance, offset): "Find a text block by the given character offset in a wasteInstance." charhit = chr(wasteInstance.WEGetChar(offset)) # check for grouping characters searchpair = '' groupers = '()[]{}' if charhit in groupers: x = string.find(groupers, charhit) if x%2: dir = -1 else: dir = 1 searchpair = groupers[int(x/2)*2 : int(x/2)*2+2] # if we have an ordinary grouping pair, find its mate and select that if searchpair: end = offset+dir depth = dir inquote = None while 1: c = chr(wasteInstance.WEGetChar(end)) if c == '\0': return (offset,end) # end of file if inquote: if c == inquote: if chr(wasteInstance.WEGetChar(end-1)) != '\\': inquote = None else: if c == searchpair[0]: depth = depth+1 if not depth: return (end,offset+1) # range found elif c == searchpair[1]: depth = depth-1 if not depth: return (offset,end+1) # range found elif c == '"' or c == "'": inquote = c end = end+dir # if we hit a quotation mark, our job is harder; # we have to start at the top of the file in order to figure out # whether this is an opening or closing quote if charhit == "'" or charhit == '"': return QuoteBlock(wasteInstance, offset) # if no grouping characters of any kind, then just select the word pos0 = offset while chr(wasteInstance.WEGetChar(pos0)) not in kWordDelims: pos0 = pos0 - 1 pos1 = offset while chr(wasteInstance.WEGetChar(pos1)) not in kWordDelims: pos1 = pos1 + 1 return (pos0+1, pos1) class DirtTracker: """This class keeps track of ranges which are dirty (i.e., need to be updated or processed), even when some of those may overlap. It can then return a range at a time for processing. Experimental: it also keeps one open triple-quote range, for tracking where you've got a triple-quote in progress.""" def __init__(self): self.ranges = [] self.tripleQ = None # when there's a """ open, this is [from,to] offsets def PushRange(self, range): "Add a range to our list of ranges that need updating." # Combine any overlapping ranges for max. efficiency. for r in self.ranges: if r[0] < range[1] and range[0] < r[1]: r[0] = min(r[0], range[0]) r[1] = max(r[1], range[1]) return self.ranges.append(list(range)) # self.Spew() def PopRange(self): "Return a range that needs updating, and remove it from the list." if not self.ranges: return None out = self.ranges[-1] del self.ranges[-1] # self.Spew() return out def Adjust(self, adjustLoc=0, adjustment=1): "Add adjustment to every element >= adjustLoc." for r in self.ranges: if r[0] >= adjustLoc: r[0] = r[0] + adjustment if r[1] >= adjustLoc: r[1] = r[1] + adjustment if self.tripleQ: if self.tripleQ[0] >= adjustLoc: self.tripleQ[0] = self.tripleQ[0] + adjustment if self.tripleQ[1] >= adjustLoc: self.tripleQ[1] = self.tripleQ[1] + adjustment #def Spew(self): # "Print current ranges to sys.stdout." # print "Dirty:", self.ranges class PyAdvancedEditor(W.PyEditor): "a customized Python source-editing widget with syntax coloring and other goodies" def __init__(self, possize, text = "", callback = None, inset = (4, 4), fontsettings = ("Python-Sans", 0, 9, (0, 0, 0)), tabsettings = (32, 0), readonly = 0, debugger = None, file = ''): W.PyEditor.__init__(self, possize, text, callback, inset, fontsettings, tabsettings, readonly, debugger, file) self.dirtTracker = DirtTracker() self.maxDirtSize = 40*60 # about 40 lines? self.getenvironment = None # assigned by our PyEditor; used in HandleQuestion self.useColoring = 1 self.lastClickTime = 0 # PATCH for testing: global foo foo = self def _getflags(self): return weDoUndo | weDoDrawOffscreen | weDoUseTempMem | weDoAutoScroll | weDoOutlineHilite def setcoloring(self, coloring): self.useColoring = coloring if coloring: self.markDirty( 0, self.ted.WEGetTextLength() ) else: self.defontify() def setfontsettings(self, (font, style, size, color)): style = 0 # we override style and color here! color = (0,0,0) W.PyEditor.setfontsettings(self, (font, style, size, color)) if self.useColoring: self.markDirty( 0, self.ted.WEGetTextLength() ) def click(self, point, modifiers): "A modified click handler, to improve word breaking etc." clickpos = self.ted.WEGetOffset(point)[0] if modifiers in kClickMappings.keys(): exec(kClickMappings[modifiers]) return prevsel = self.ted.WEGetSelection() if clickpos == prevsel[0] and clickpos == prevsel[1] and \ Evt.TickCount() - self.lastClickTime < Evt.GetDblTime(): # skip the usual processing, and find the range to select here instead block = TextBlock(self.ted, clickpos) self.ted.WESetSelection(block[0], block[1]) DragSelectWords(self.ted) else: # not a double-click -- process normally W.PyEditor.click(self, point, modifiers) self.lastClickTime = Evt.TickCount() def checkLine(self, offset): """Check the line containing the given offset, and see whether it begins or ends a triple-quote. Update lineNotes accordingly.""" linerange = self.ted.WEFindLine( offset, kLeadingEdge ) line = self.get()[linerange[0] : linerange[1]] # isn't there an easier way?!? qpos = string.find(line, '"""') if qpos < 0: return # no triple quotes started or ended here! # ok, so it looks like there's a """ on this line... def markDirty(self, fromWhere, toWhere ): """Mark the given fromWhere,toWhere range as dirty, padding to lines and breaking into smaller chunks as necessary.""" # expand to the start and end of the line beginning = self.ted.WEFindLine( fromWhere, kLeadingEdge )[0] ending = self.ted.WEFindLine( toWhere, kLeadingEdge )[1] # also check whether we're within an open triple-quote... # if we are, expand to fill that range if self.dirtTracker.tripleQ and ending >= self.dirtTracker.tripleQ[0] \ and beginning <= self.dirtTracker.tripleQ[1]: beginning = min(beginning, self.dirtTracker.tripleQ[0]) ending = max(ending, self.dirtTracker.tripleQ[1]) else: # also check the run range -- see if this line's already in a string # (and if so, expand to include the whole string) runInfo = self.ted.WEGetRunInfo( beginning ) runRange = runInfo[:2] runColor = runInfo[4][3] if runColor == kStringColor: beginning = runRange[0] ending = runRange[1] if ending - beginning > self.maxDirtSize: parts = range(beginning, ending, self.maxDirtSize) + [ending] for i in range(0, len(parts)-1): self.dirtTracker.PushRange( (parts[i], parts[i+1]) ) return self.dirtTracker.PushRange( (beginning, ending) ) def fontify(self): self.fontifypart() def fontifyselection(self): selstart, selend = self.ted.WEGetSelection() self.fontifypart(selstart, selend) def fontifyDirty(self): part = self.dirtTracker.PopRange() if part: # print "Fontifying:", part self.fontifypart( part[0], part[1] ) def defontify(self, start=0, end=None): W.SetCursor('watch') self.ted.WEFeatureFlag(weFOutlineHilite, 0) self.ted.WEDeactivate() self.ted.WEFeatureFlag(weFAutoScroll, 0) self.ted.WEFeatureFlag(weFUndo, 0) pytext = string.join(string.split(self.get(), '\r'), '\n') if end is None: end = len(pytext) selstart, selend = self.ted.WEGetSelection() self.ted.WESetSelection(start, end) fontsettings = self.fontsettings fontsettings = (Fm.GetFNum(self.fontsettings[0]),) + self.fontsettings[1:] self.ted.WESetStyle(weDoFace | weDoColor | weDoFont | weDoSize, fontsettings) self.ted.WESetSelection(selstart, selend) self.ted.WEFeatureFlag(weFAutoScroll, 1) self.ted.WEFeatureFlag(weFUndo, 1) self.ted.WEActivate() self.ted.WEFeatureFlag(weFOutlineHilite, 1) def fontifypart(self, start = 0, end = None): W.SetCursor('watch') self.ted.WEFeatureFlag(weFOutlineHilite, 0) self.ted.WEDeactivate() #self.ted.WEFeatureFlag(weFInhibitRecal, 1) #self.ted.WEFeatureFlag(weFInhibitRedraw, 1) self.ted.WEFeatureFlag(weFAutoScroll, 0) self.ted.WEFeatureFlag(weFUndo, 0) pytext = string.join(string.split(self.get(), '\r'), '\n') if end is None: end = len(pytext) selstart, selend = self.ted.WEGetSelection() self.ted.WESetSelection(start, end) fontsettings = (Fm.GetFNum(self.fontsettings[0]),) + self.fontsettings[1:] self.ted.WESetStyle(weDoFace | weDoColor | weDoFont | weDoSize, fontsettings) tags = PyFontify.fontify(pytext, start, end) styles = { 'string': (weDoColor, (0, 0, 0, kStringColor)), 'keyword': (weDoFace, (0, 1, 0, kKeywordColor)), 'comment': (weDoFace | weDoColor, (0, 2, 0, kCommentColor)), 'identifier': (weDoColor, (0, 0, 0, kIdentifierColor)) } setselection = self.ted.WESetSelection setstyle = self.ted.WESetStyle for tag, start, end, sublist in tags: setselection(start, end) mode, style = styles[tag] setstyle(mode, style) self.ted.WESetSelection(selstart, selend) self.SetPort() #Qd.EraseRect(self.ted.WEGetViewRect()) #self.ted.WEFeatureFlag(weFInhibitRedraw, 0) #self.ted.WEFeatureFlag(weFInhibitRecal, 0) self.ted.WEFeatureFlag(weFAutoScroll, 1) self.ted.WEFeatureFlag(weFUndo, 1) self.ted.WEActivate() self.ted.WEFeatureFlag(weFOutlineHilite, 1) #self.ted.WECalText() #self.ted.WEUpdate(self._parentwindow.wid.GetWindowPort().visRgn) def idle(self): "Fontify the dirty parts, then do regular idling." if self.useColoring: self.fontifyDirty() W.PyEditor.idle(self) def handleTripleQuote(self, offset): "Handle a newly entered triple-quote at location 'offset'." def InsertChunk(self, offset): "Insert a chunk of text at the given character position, to the current insertion point." start, end = TextBlock(self.ted, offset) text = '' for i in range(start,end): text = text + chr(self.ted.WEGetChar(i)) self.ted.WEInsert(text, None, None) def HandleQuestion(self, offset): "Handle a question mark by looking up the preceeding function. \ Return 1/0 for success/fail." # find the preceeding function word = '' pos = offset while 1: c = chr(self.ted.WEGetChar(pos)) if (c not in kWordDelims) or c == '.': word = word + c else: break pos = pos + 1 pos = offset while 1: pos = pos - 1 c = chr(self.ted.WEGetChar(pos)) if (c not in kWordDelims) or c == '.': word = c + word else: break print "\nQuerying:", word # now, we need to find the function matching this word... try: # try looking it up in the code's environment globals, file, modname = self.getenvironment() f = eval(word, globals) except: f = None # try looking it up in our own namespace try: f = eval(word) except: pass if not callable(f): # try splitting it into parts, and importing the first part modname = string.split(word,'.')[0] try: mod = __import__(modname) f = mod.__dict__[string.split(word,'.')[1]] except:pass try: d = dir(f) if "func_code" in d: defoffset = f.func_code.co_argcount - len(f.func_defaults) args = [] for i in range(0, f.func_code.co_argcount): args.append( f.func_code.co_varnames[i] ) if i >= defoffset: args[-1] = args[-1] + "=%s" % f.func_defaults[i-defoffset] print "%s(%s)" % (f.func_name, string.join(args,',')) if f.__doc__: print f.__doc__ else: print f return 1 except: return 0 def LaunchURL(self, pixelpos): "Launch the URL in the text at the given pixel position." # this is something built into the standard editor, # so we just need to make it think we command-clicked W.PyEditor.click(self, pixelpos, Events.cmdKey) def key(self, char, event): # if it's not a typing key, then just do the inherited stuff # LATER: watch for special keys like delete, del, etc. if (char != chr(8) and char < ' ' or char > chr(127)): #print "char:", ord(char) W.PyEditor.key(self, char, event) return selRange = self.ted.WEGetSelection() # check for special case of "?" (for function info) if char == '?': if self.HandleQuestion(selRange[0]): return # query was successful, so don't enter the ? mark # for now, let's assume it's a typing key effectFrom = self.ted.WEFindLine(selRange[0], kLeadingEdge) effectTo = self.ted.WEFindLine(selRange[1], kLeadingEdge) fullEffect = (effectFrom[0], effectTo[1]) # so we're replacing selRange with 0 or 1 chars... if char == chr(8): newlen = 0 # delete key -- no new chars else: newlen = 1 # anything else -- 1 new char # this requires an adjustment self.dirtTracker.Adjust( adjustLoc=selRange[0], adjustment=-(selRange[1]-selRange[0])+newlen ) #print "Got key %s affecting %s" % (char, fullEffect) # do the key W.PyEditor.key(self, char, event) # check for special case of triple-quotes if char == '"': quote = ord(char) if self.ted.WEGetChar(selRange[0]-1) == quote and \ self.ted.WEGetChar(selRange[0]-2) == quote: # we just typed a triple quote!!! self.handleTripleQuote(selRange[0]-2) # now, note what range needs updated self.markDirty( effectFrom[0], effectFrom[0] ) def open(self): W.PyEditor.open(self) self.markDirty( 0, self.ted.WEGetTextLength() ) def domenu_clear(self, *args): selRange = self.ted.WEGetSelection() self.dirtTracker.Adjust( adjustLoc=selRange[0], adjustment=-(selRange[1]-selRange[0]) ) W.PyEditor.domenu_clear(self, args) self.markDirty( selRange[0], selRange[0] ) def domenu_cut(self, *args): selRange = self.ted.WEGetSelection() self.dirtTracker.Adjust( adjustLoc=selRange[0], adjustment=-(selRange[1]-selRange[0]) ) W.PyEditor.domenu_cut(self, args) self.markDirty( selRange[0], selRange[0] ) def domenu_paste(self, *args): selRange = self.ted.WEGetSelection() # adjustment depends on how big the text pasted is! # we can get that by doing the paste, then checking the cursor again. W.PyEditor.domenu_paste(self, args) newRange = self.ted.WEGetSelection() adjustment = newRange[0] - (selRange[1]-selRange[0]) self.dirtTracker.Adjust( adjustLoc=selRange[0], adjustment=adjustment ) # then, note that the newly pasted stuff is dirty self.markDirty( selRange[0], newRange[0] ) def domenu_comment(self): W.PyEditor.domenu_comment(self) selRange = self.ted.WEGetSelection() self.markDirty( selRange[0], selRange[1] ) def domenu_uncomment(self): W.PyEditor.domenu_uncomment(self) selRange = self.ted.WEGetSelection() self.markDirty( selRange[0], selRange[1] )