1import sys 2import traceback 3import sets 4import keyword 5import time 6from Foundation import * 7from AppKit import * 8from PyObjCTools import NibClassBuilder, AppHelper 9 10NibClassBuilder.extractClasses("RemotePyInterpreterDocument.nib") 11 12from AsyncPythonInterpreter import * 13from ConsoleReactor import * 14from netrepr import RemoteObjectReference 15 16def ensure_unicode(s): 17 if not isinstance(s, unicode): 18 s = unicode(s, 'utf-8', 'replace') 19 return s 20 21class RemotePyInterpreterReactor(NibClassBuilder.AutoBaseClass): 22 def handleExpectCommand_(self, command): 23 print command 24 seq = command[0] 25 name = command[1] 26 args = command[2:] 27 netrepr = self.netReprCenter.netrepr 28 rval = None 29 code = None 30 if name == 'RemoteConsole.raw_input': 31 prompt = ensure_unicode(args[0]) 32 def input_received(line): 33 self.sendResult_sequence_(line, seq) 34 self.delegate.expectCodeInput_withPrompt_(input_received, prompt) 35 elif name == 'RemoteConsole.write': 36 args = [ensure_unicode(args[0]), u'code'] 37 self.doCallback_sequence_args_(self.delegate.writeString_forOutput_, seq, args) 38 elif name == 'RemoteConsole.displayhook': 39 obj = args[0] 40 def displayhook_respond(reprobject): 41 self.delegate.writeString_forOutput_(ensure_unicode(reprobject) + u'\n', u'code') 42 def displayhook_local(obj): 43 if obj is not None: 44 displayhook_respond(repr(obj)) 45 if isinstance(obj, RemoteObjectReference): 46 self.deferCallback_sequence_value_(displayhook_respond, seq, 'repr(%s)' % (netrepr(obj),)) 47 else: 48 self.doCallback_sequence_args_(displayhook_local, seq, args) 49 elif name.startswith('RemoteFileLike.'): 50 method = name[len('RemoteFileLike.'):] 51 if method == 'write': 52 style, msg = map(ensure_unicode, args) 53 args = [msg, style] 54 self.doCallback_sequence_args_(self.delegate.writeString_forOutput_, seq, args) 55 56 elif method == 'readline': 57 def input_received(line): 58 self.sendResult_sequence_(line, seq) 59 self.delegate.expectCodeInput_withPrompt_(input_received, '') 60 61 else: 62 self.doCallback_sequence_args_(NSLog, seq, [u'%r does not respond to expect %r' % (self, command,)]) 63 elif name == 'RemoteConsole.initialize': 64 def gotTitle(repr_versioninfo, executable, pid): 65 self.delegate.setVersion_executable_pid_( 66 u'.'.join(map(unicode, self.netEval_(repr_versioninfo)[:3])), 67 ensure_unicode(executable), 68 pid, 69 ) 70 self.doCallback_sequence_args_(gotTitle, seq, args) 71 # fh = getattr(sys, args[0]) 72 # meth = getattr(fh, name[len('RemoteFileLike.'):]) 73 # self.doCallback_sequence_args_(meth, seq, args[1:]) 74 else: 75 self.doCallback_sequence_args_(NSLog, seq, [u'%r does not respond to expect %r' % (self, command,)]) 76 77 def close(self): 78 super(RemotePyInterpreterReactor, self).close() 79 self.delegate = None 80 81 82class PseudoUTF8Input(object): 83 softspace = 0 84 def __init__(self, readlinemethod): 85 self._buffer = u'' 86 self._readline = readlinemethod 87 88 def read(self, chars=None): 89 if chars is None: 90 if self._buffer: 91 rval = self._buffer 92 self._buffer = u'' 93 if rval.endswith(u'\r'): 94 rval = rval[:-1]+u'\n' 95 return rval.encode('utf-8') 96 else: 97 return self._readline(u'\x04')[:-1].encode('utf-8') 98 else: 99 while len(self._buffer) < chars: 100 self._buffer += self._readline(u'\x04\r') 101 if self._buffer.endswith('\x04'): 102 self._buffer = self._buffer[:-1] 103 break 104 rval, self._buffer = self._buffer[:chars], self._buffer[chars:] 105 return rval.encode('utf-8').replace('\r','\n') 106 107 def readline(self): 108 if u'\r' not in self._buffer: 109 self._buffer += self._readline(u'\x04\r') 110 if self._buffer.endswith('\x04'): 111 rval = self._buffer[:-1].encode('utf-8') 112 elif self._buffer.endswith('\r'): 113 rval = self._buffer[:-1].encode('utf-8')+'\n' 114 self._buffer = u'' 115 116 return rval 117 118 119DEBUG_DELEGATE = 0 120PASSTHROUGH = ( 121 'deleteBackward:', 122 'complete:', 123 'moveRight:', 124 'moveLeft:', 125) 126 127class RemotePyInterpreterDocument(NibClassBuilder.AutoBaseClass): 128 """ 129 PyInterpreter is a delegate/controller for a NSTextView, 130 turning it into a full featured interactive Python interpreter. 131 """ 132 133 def expectCodeInput_withPrompt_(self, callback, prompt): 134 self.writeString_forOutput_(prompt, u'code') 135 self.setCharacterIndexForInput_(self.lengthOfTextView()) 136 self.p_input_callbacks.append(callback) 137 self.flushCallbacks() 138 139 def flushCallbacks(self): 140 while self.p_input_lines and self.p_input_callbacks: 141 self.p_input_callbacks.pop(0)(self.p_input_lines.pop(0)) 142 143 def setupTextView(self): 144 self.textView.setFont_(self.font()) 145 self.textView.setContinuousSpellCheckingEnabled_(False) 146 self.textView.setRichText_(False) 147 self.setCharacterIndexForInput_(0) 148 149 def setVersion_executable_pid_(self, version, executable, pid): 150 self.version = version 151 self.pid = pid 152 self.executable = executable 153 self.setFileName_(executable) 154 155 def displayName(self): 156 if not hasattr(self, 'version'): 157 return u'Starting...' 158 return u'Python %s - %s - %s' % (self.version, self.executable, self.pid) 159 160 def updateChangeCount_(self, val): 161 return 162 163 def windowWillClose_(self, window): 164 if self.commandReactor is not None: 165 self.commandReactor.close() 166 self.commandReactor = None 167 if self.interpreter is not None: 168 self.interpreter.close() 169 self.interpreter = None 170 171 def windowNibName(self): 172 return u'RemotePyInterpreterDocument' 173 174 def isDocumentEdited(self): 175 return False 176 177 def awakeFromNib(self): 178 # XXX - should this be done later? 179 self.setFont_(NSFont.userFixedPitchFontOfSize_(10)) 180 self.p_colors = { 181 u'stderr': NSColor.redColor(), 182 u'stdout': NSColor.blueColor(), 183 u'code': NSColor.blackColor(), 184 } 185 self.setHistoryLength_(50) 186 self.setHistoryView_(0) 187 self.setInteracting_(False) 188 self.setAutoScroll_(True) 189 self.setSingleLineInteraction_(False) 190 self.p_history = [u''] 191 self.p_input_callbacks = [] 192 self.p_input_lines = [] 193 self.setupTextView() 194 self.interpreter.connect() 195 196 # 197 # Modal input dialog support 198 # 199 200 #def p_nestedRunLoopReaderUntilEOLchars_(self, eolchars): 201 # """ 202 # This makes the baby jesus cry. 203 204 # I want co-routines. 205 # """ 206 # app = NSApplication.sharedApplication() 207 # window = self.textView.window() 208 # self.setCharacterIndexForInput_(self.lengthOfTextView()) 209 # # change the color.. eh 210 # self.textView.setTypingAttributes_({ 211 # NSFontAttributeName: self.font(), 212 # NSForegroundColorAttributeName: self.colorForName_(u'code'), 213 # }) 214 # while True: 215 # event = app.nextEventMatchingMask_untilDate_inMode_dequeue_( 216 # NSAnyEventMask, 217 # NSDate.distantFuture(), 218 # NSDefaultRunLoopMode, 219 # True) 220 # if (event.type() == NSKeyDown) and (event.window() is window): 221 # eol = event.characters() 222 # if eol in eolchars: 223 # break 224 # app.sendEvent_(event) 225 # cl = self.currentLine() 226 # if eol == u'\r': 227 # self.writeNewLine() 228 # return cl + eol 229 230 def executeLine_(self, line): 231 self.addHistoryLine_(line) 232 self.p_input_lines.append(line) 233 self.flushCallbacks() 234 self.p_history = filter(None, self.p_history) 235 self.p_history.append(u'') 236 self.setHistoryView_(len(self.p_history) - 1) 237 238 def executeInteractiveLine_(self, line): 239 self.setInteracting_(True) 240 try: 241 self.executeLine_(line) 242 finally: 243 self.setInteracting_(False) 244 245 def replaceLineWithCode_(self, s): 246 idx = self.characterIndexForInput() 247 ts = self.textView.textStorage() 248 s = self.formatString_forOutput_(s, u'code') 249 ts.replaceCharactersInRange_withAttributedString_( 250 (idx, len(ts.mutableString())-idx), 251 s, 252 ) 253 254 # 255 # History functions 256 # 257 258 def addHistoryLine_(self, line): 259 line = line.rstrip(u'\n') 260 if self.p_history[-1] == line: 261 return False 262 if not line: 263 return False 264 self.p_history.append(line) 265 if len(self.p_history) > self.historyLength(): 266 self.p_history.pop(0) 267 return True 268 269 def historyDown_(self, sender): 270 if self.p_historyView == (len(self.p_history) - 1): 271 return 272 self.p_history[self.p_historyView] = self.currentLine() 273 self.p_historyView += 1 274 self.replaceLineWithCode_(self.p_history[self.p_historyView]) 275 self.moveToEndOfLine_(self) 276 277 def historyUp_(self, sender): 278 if self.p_historyView == 0: 279 return 280 self.p_history[self.p_historyView] = self.currentLine() 281 self.p_historyView -= 1 282 self.replaceLineWithCode_(self.p_history[self.p_historyView]) 283 self.moveToEndOfLine_(self) 284 285 # 286 # Convenience methods to create/write decorated text 287 # 288 289 def formatString_forOutput_(self, s, name): 290 return NSAttributedString.alloc().initWithString_attributes_( 291 s, 292 { 293 NSFontAttributeName: self.font(), 294 NSForegroundColorAttributeName: self.colorForName_(name), 295 }, 296 ) 297 298 def writeString_forOutput_(self, s, name): 299 s = self.formatString_forOutput_(s, name) 300 self.textView.textStorage().appendAttributedString_(s) 301 if self.isAutoScroll(): 302 self.textView.scrollRangeToVisible_((self.lengthOfTextView(), 0)) 303 304 def writeNewLine(self): 305 self.writeString_forOutput_(u'\n', u'code') 306 307 def colorForName_(self, name): 308 return self.p_colors[name] 309 310 def setColor_forName_(self, color, name): 311 self.p_colors[name] = color 312 313 # 314 # Convenience methods for manipulating the NSTextView 315 # 316 317 def currentLine(self): 318 return self.textView.textStorage().mutableString()[self.characterIndexForInput():] 319 320 def moveAndScrollToIndex_(self, idx): 321 self.textView.scrollRangeToVisible_((idx, 0)) 322 self.textView.setSelectedRange_((idx, 0)) 323 324 def lengthOfTextView(self): 325 return len(self.textView.textStorage().mutableString()) 326 327 # 328 # NSTextViewDelegate methods 329 # 330 331 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, aTextView, completions, (begin, length), index): 332 # XXX 333 # this will probably have to be tricky in order to be asynchronous.. 334 # either by: 335 # nesting a run loop (bleh) 336 # polling the subprocess (bleh) 337 # returning nothing and calling self.textView.complete_ later 338 return None, 0 339 340 if False: 341 txt = self.textView.textStorage().mutableString() 342 end = begin+length 343 while (begin>0) and (txt[begin].isalnum() or txt[begin] in u'._'): 344 begin -= 1 345 while not txt[begin].isalnum(): 346 begin += 1 347 return self.p_console.recommendCompletionsFor(txt[begin:end]) 348 349 def textView_shouldChangeTextInRange_replacementString_(self, aTextView, aRange, newString): 350 begin, length = aRange 351 lastLocation = self.characterIndexForInput() 352 if begin < lastLocation: 353 # no editing anywhere but the interactive line 354 return False 355 newString = newString.replace(u'\r', u'\n') 356 if u'\n' in newString: 357 if begin != lastLocation: 358 # no pasting multiline unless you're at the end 359 # of the interactive line 360 return False 361 # multiline paste support 362 #self.clearLine() 363 newString = self.currentLine() + newString 364 for s in newString.strip().split(u'\n'): 365 self.writeString_forOutput_(s + u'\n', u'code') 366 self.executeLine_(s) 367 return False 368 return True 369 370 def textView_willChangeSelectionFromCharacterRange_toCharacterRange_(self, aTextView, fromRange, toRange): 371 begin, length = toRange 372 if self.singleLineInteraction() and length == 0 and begin < self.characterIndexForInput(): 373 # no cursor movement off the interactive line 374 return fromRange 375 else: 376 return toRange 377 378 def textView_doCommandBySelector_(self, aTextView, aSelector): 379 # deleteForward: is ctrl-d 380 if self.isInteracting(): 381 if aSelector == 'insertNewline:': 382 self.writeNewLine() 383 return False 384 # XXX - this is ugly 385 responder = getattr(self, aSelector.replace(':','_'), None) 386 if responder is not None: 387 responder(aTextView) 388 return True 389 else: 390 if DEBUG_DELEGATE and aSelector not in PASSTHROUGH: 391 print aSelector 392 return False 393 394 # 395 # doCommandBySelector "posers" on the textView 396 # 397 398 def insertTabIgnoringFieldEditor_(self, sender): 399 # this isn't terribly necessary, b/c F5 and opt-esc do completion 400 # but why not 401 sender.complete_(self) 402 403 def moveToBeginningOfLine_(self, sender): 404 self.moveAndScrollToIndex_(self.characterIndexForInput()) 405 406 def moveToEndOfLine_(self, sender): 407 self.moveAndScrollToIndex_(self.lengthOfTextView()) 408 409 def moveToBeginningOfLineAndModifySelection_(self, sender): 410 begin, length = self.textView.selectedRange() 411 pos = self.characterIndexForInput() 412 if begin + length > pos: 413 self.textView.setSelectedRange_((pos, begin + length - pos)) 414 else: 415 self.moveToBeginningOfLine_(sender) 416 417 def moveToEndOfLineAndModifySelection_(self, sender): 418 begin, length = self.textView.selectedRange() 419 pos = max(self.characterIndexForInput(), begin) 420 self.textView.setSelectedRange_((pos, self.lengthOfTextView())) 421 422 def insertNewline_(self, sender): 423 line = self.currentLine() 424 self.writeNewLine() 425 self.executeInteractiveLine_(line) 426 427 moveToBeginningOfParagraph_ = moveToBeginningOfLine_ 428 moveToEndOfParagraph_ = moveToEndOfLine_ 429 insertNewlineIgnoringFieldEditor_ = insertNewline_ 430 moveDown_ = historyDown_ 431 moveUp_ = historyUp_ 432 433 # 434 # Accessors 435 # 436 437 def historyLength(self): 438 return self.p_historyLength 439 440 def setHistoryLength_(self, length): 441 self.p_historyLength = length 442 443 def font(self): 444 return self.p_font 445 446 def setFont_(self, font): 447 self.p_font = font 448 449 def isInteracting(self): 450 return self.p_interacting 451 452 def setInteracting_(self, v): 453 self.p_interacting = v 454 455 def isAutoScroll(self): 456 return self.p_autoScroll 457 458 def setAutoScroll_(self, v): 459 self.p_autoScroll = v 460 461 def characterIndexForInput(self): 462 return self.p_characterIndexForInput 463 464 def setCharacterIndexForInput_(self, idx): 465 self.p_characterIndexForInput = idx 466 self.moveAndScrollToIndex_(idx) 467 468 def historyView(self): 469 return self.p_historyView 470 471 def setHistoryView_(self, v): 472 self.p_historyView = v 473 474 def singleLineInteraction(self): 475 return self.p_singleLineInteraction 476 477 def setSingleLineInteraction_(self, v): 478 self.p_singleLineInteraction = v 479 480 481 482if __name__ == '__main__': 483 AppHelper.runEventLoop(installInterrupt=True) 484