##// END OF EJS Templates
branch to try non-textview
Barry Wark -
Show More
@@ -1,548 +1,560 b''
1 1 # encoding: utf-8
2 2 # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*-
3 3
4 4 """PyObjC classes to provide a Cocoa frontend to the
5 5 IPython.kernel.engineservice.IEngineBase.
6 6
7 7 To add an IPython interpreter to a cocoa app, instantiate an
8 8 IPythonCocoaController in a XIB and connect its textView outlet to an
9 9 NSTextView instance in your UI. That's it.
10 10
11 11 Author: Barry Wark
12 12 """
13 13
14 14 __docformat__ = "restructuredtext en"
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Copyright (C) 2008 The IPython Development Team
18 18 #
19 19 # Distributed under the terms of the BSD License. The full license is in
20 20 # the file COPYING, distributed as part of this software.
21 21 #-----------------------------------------------------------------------------
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Imports
25 25 #-----------------------------------------------------------------------------
26 26
27 27 import sys
28 28 import objc
29 29 import uuid
30 30
31 31 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
32 32 NSLog, NSNotificationCenter, NSMakeRange,\
33 33 NSLocalizedString, NSIntersectionRange,\
34 34 NSString, NSAutoreleasePool
35 35
36 36 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
37 37 NSTextView, NSRulerView, NSVerticalRuler
38 38
39 39 from pprint import saferepr
40 40
41 41 import IPython
42 42 from IPython.kernel.engineservice import ThreadedEngineService
43 43 from IPython.frontend.frontendbase import AsyncFrontEndBase
44 44
45 45 from twisted.internet.threads import blockingCallFromThread
46 46 from twisted.python.failure import Failure
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # Classes to implement the Cocoa frontend
50 50 #-----------------------------------------------------------------------------
51 51
52 52 # TODO:
53 53 # 1. use MultiEngineClient and out-of-process engine rather than
54 54 # ThreadedEngineService?
55 55 # 2. integrate Xgrid launching of engines
56 56
57 57 class AutoreleasePoolWrappedThreadedEngineService(ThreadedEngineService):
58 58 """Wrap all blocks in an NSAutoreleasePool"""
59 59
60 60 def wrapped_execute(self, msg, lines):
61 61 """wrapped_execute"""
62 62 try:
63 63 p = NSAutoreleasePool.alloc().init()
64 64 result = super(AutoreleasePoolWrappedThreadedEngineService,
65 65 self).wrapped_execute(msg, lines)
66 66 finally:
67 67 p.drain()
68 68
69 69 return result
70 70
71 71
72 72
73 class Cell(NSObject):
74 """
75 Representation of the prompts, input and output of a cell in the
76 frontend
77 """
78
79 blockNumber = objc.ivar().unsigned_long()
80 blockID = objc.ivar()
81 inputBlock = objc.ivar()
82 output = objc.ivar()
83
84
85
73 86 class CellBlock(object):
74 87 """
75 88 Storage for information about text ranges relating to a single cell
76 89 """
77 90
78 91
79 92 def __init__(self, inputPromptRange, inputRange=None, outputPromptRange=None,
80 93 outputRange=None):
81 94 super(CellBlock, self).__init__()
82 95 self.inputPromptRange = inputPromptRange
83 96 self.inputRange = inputRange
84 97 self.outputPromptRange = outputPromptRange
85 98 self.outputRange = outputRange
86 99
87 100 def update_ranges_for_insertion(self, text, textRange):
88 101 """Update ranges for text insertion at textRange"""
89 102
90 103 for r in [self.inputPromptRange,self.inputRange,
91 104 self.outputPromptRange, self.outputRange]:
92 105 if(r == None):
93 106 continue
94 107 intersection = NSIntersectionRange(r,textRange)
95 108 if(intersection.length == 0): #ranges don't intersect
96 109 if r.location >= textRange.location:
97 110 r.location += len(text)
98 111 else: #ranges intersect
99 112 if(r.location > textRange.location):
100 113 offset = len(text) - intersection.length
101 114 r.length -= offset
102 115 r.location += offset
103 116 elif(r.location == textRange.location):
104 117 r.length += len(text) - intersection.length
105 118 else:
106 119 r.length -= intersection.length
107 120
108 121
109 122 def update_ranges_for_deletion(self, textRange):
110 123 """Update ranges for text deletion at textRange"""
111 124
112 125 for r in [self.inputPromptRange,self.inputRange,
113 126 self.outputPromptRange, self.outputRange]:
114 127 if(r==None):
115 128 continue
116 129 intersection = NSIntersectionRange(r, textRange)
117 130 if(intersection.length == 0): #ranges don't intersect
118 131 if r.location >= textRange.location:
119 132 r.location -= textRange.length
120 133 else: #ranges intersect
121 134 if(r.location > textRange.location):
122 135 offset = intersection.length
123 136 r.length -= offset
124 137 r.location += offset
125 138 elif(r.location == textRange.location):
126 139 r.length += intersection.length
127 140 else:
128 141 r.length -= intersection.length
129 142
130 143 def __repr__(self):
131 144 return 'CellBlock('+ str((self.inputPromptRange,
132 145 self.inputRange,
133 146 self.outputPromptRange,
134 147 self.outputRange)) + ')'
135 148
136 149
137 150
138 151
139 152 class IPythonCocoaController(NSObject, AsyncFrontEndBase):
140 153 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
141 154 waitingForEngine = objc.ivar().bool()
142 155 textView = objc.IBOutlet()
143 156
144 157 def init(self):
145 158 self = super(IPythonCocoaController, self).init()
146 159 AsyncFrontEndBase.__init__(self,
147 160 engine=AutoreleasePoolWrappedThreadedEngineService())
148 161 if(self != None):
149 162 self._common_init()
150 163
151 164 return self
152 165
153 166 def _common_init(self):
154 167 """_common_init"""
155 168
156 169 self.userNS = NSMutableDictionary.dictionary()
157 170 self.waitingForEngine = False
158 171
159 172 self.lines = {}
160 173 self.tabSpaces = 4
161 174 self.tabUsesSpaces = True
162 175 self.currentBlockID = self.next_block_ID()
163 176 self.blockRanges = {} # blockID=>CellBlock
164 177
165 178
166 179 def awakeFromNib(self):
167 180 """awakeFromNib"""
168 181
169 182 self._common_init()
170 183
171 184 # Start the IPython engine
172 185 self.engine.startService()
173 186 NSLog('IPython engine started')
174 187
175 188 # Register for app termination
176 189 nc = NSNotificationCenter.defaultCenter()
177 190 nc.addObserver_selector_name_object_(
178 191 self,
179 192 'appWillTerminate:',
180 193 NSApplicationWillTerminateNotification,
181 194 None)
182 195
183 196 self.textView.setDelegate_(self)
184 197 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
185 198 r = NSRulerView.alloc().initWithScrollView_orientation_(
186 199 self.textView.enclosingScrollView(),
187 200 NSVerticalRuler)
188 201 self.verticalRulerView = r
189 202 self.verticalRulerView.setClientView_(self.textView)
190 203 self._start_cli_banner()
191 204 self.start_new_block()
192 205
193 206
194 207 def appWillTerminate_(self, notification):
195 208 """appWillTerminate"""
196 209
197 210 self.engine.stopService()
198 211
199 212
200 213 def complete(self, token):
201 214 """Complete token in engine's user_ns
202 215
203 216 Parameters
204 217 ----------
205 218 token : string
206 219
207 220 Result
208 221 ------
209 222 Deferred result of
210 223 IPython.kernel.engineservice.IEngineBase.complete
211 224 """
212 225
213 226 return self.engine.complete(token)
214 227
215 228
216 229 def execute(self, block, blockID=None):
217 230 self.waitingForEngine = True
218 231 self.willChangeValueForKey_('commandHistory')
219 232 d = super(IPythonCocoaController, self).execute(block,
220 233 blockID)
221 234 d.addBoth(self._engine_done)
222 235 d.addCallback(self._update_user_ns)
223 236
224 237 return d
225 238
226 239
227 240 def push_(self, namespace):
228 241 """Push dictionary of key=>values to python namespace"""
229 242
230 243 self.waitingForEngine = True
231 244 self.willChangeValueForKey_('commandHistory')
232 245 d = self.engine.push(namespace)
233 246 d.addBoth(self._engine_done)
234 247 d.addCallback(self._update_user_ns)
235 248
236 249
237 250 def pull_(self, keys):
238 251 """Pull keys from python namespace"""
239 252
240 253 self.waitingForEngine = True
241 254 result = blockingCallFromThread(self.engine.pull, keys)
242 255 self.waitingForEngine = False
243 256
244 257 @objc.signature('v@:@I')
245 258 def executeFileAtPath_encoding_(self, path, encoding):
246 259 """Execute file at path in an empty namespace. Update the engine
247 260 user_ns with the resulting locals."""
248 261
249 262 lines,err = NSString.stringWithContentsOfFile_encoding_error_(
250 263 path,
251 264 encoding,
252 265 None)
253 266 self.engine.execute(lines)
254 267
255 268
256 269 def _engine_done(self, x):
257 270 self.waitingForEngine = False
258 271 self.didChangeValueForKey_('commandHistory')
259 272 return x
260 273
261 274 def _update_user_ns(self, result):
262 275 """Update self.userNS from self.engine's namespace"""
263 276 d = self.engine.keys()
264 277 d.addCallback(self._get_engine_namespace_values_for_keys)
265 278
266 279 return result
267 280
268 281
269 282 def _get_engine_namespace_values_for_keys(self, keys):
270 283 d = self.engine.pull(keys)
271 284 d.addCallback(self._store_engine_namespace_values, keys=keys)
272 285
273 286
274 287 def _store_engine_namespace_values(self, values, keys=[]):
275 288 assert(len(values) == len(keys))
276 289 self.willChangeValueForKey_('userNS')
277 290 for (k,v) in zip(keys,values):
278 291 self.userNS[k] = saferepr(v)
279 292 self.didChangeValueForKey_('userNS')
280 293
281 294
282 295 def update_cell_prompt(self, result, blockID=None):
283 296 print self.blockRanges
284 297 if(isinstance(result, Failure)):
285 298 prompt = self.input_prompt()
286 299
287 300 else:
288 301 prompt = self.input_prompt(number=result['number'])
289 302
290 303 r = self.blockRanges[blockID].inputPromptRange
291 304 self.insert_text(prompt,
292 305 textRange=r,
293 306 scrollToVisible=False
294 307 )
295 308
296 309 return result
297 310
298 311
299 312 def render_result(self, result):
300 313 blockID = result['blockID']
301 314 inputRange = self.blockRanges[blockID].inputRange
302 315 del self.blockRanges[blockID]
303 316
304 317 #print inputRange,self.current_block_range()
305 318 self.insert_text('\n' +
306 319 self.output_prompt(number=result['number']) +
307 320 result.get('display',{}).get('pprint','') +
308 321 '\n\n',
309 322 textRange=NSMakeRange(inputRange.location+inputRange.length,
310 323 0))
311 324 return result
312 325
313 326
314 327 def render_error(self, failure):
315 328 print failure
316 329 blockID = failure.blockID
317 330 inputRange = self.blockRanges[blockID].inputRange
318 331 self.insert_text('\n' +
319 332 self.output_prompt() +
320 333 '\n' +
321 334 failure.getErrorMessage() +
322 335 '\n\n',
323 336 textRange=NSMakeRange(inputRange.location +
324 337 inputRange.length,
325 338 0))
326 339 self.start_new_block()
327 340 return failure
328 341
329 342
330 343 def _start_cli_banner(self):
331 344 """Print banner"""
332 345
333 346 banner = """IPython1 %s -- An enhanced Interactive Python.""" % \
334 347 IPython.__version__
335 348
336 349 self.insert_text(banner + '\n\n')
337 350
338 351
339 352 def start_new_block(self):
340 353 """"""
341 354
342 355 self.currentBlockID = self.next_block_ID()
343 356 self.blockRanges[self.currentBlockID] = self.new_cell_block()
344 357 self.insert_text(self.input_prompt(),
345 358 textRange=self.current_block_range().inputPromptRange)
346 359
347 360
348 361
349 362 def next_block_ID(self):
350 363
351 364 return uuid.uuid4()
352 365
353 366 def new_cell_block(self):
354 367 """A new CellBlock at the end of self.textView.textStorage()"""
355 368
356 return CellBlock(NSMakeRange(self.textView.textStorage().length()-1,
369 return CellBlock(NSMakeRange(self.textView.textStorage().length(),
357 370 0), #len(self.input_prompt())),
358 NSMakeRange(self.textView.textStorage().length()-1,# + len(self.input_prompt()),
371 NSMakeRange(self.textView.textStorage().length(),# + len(self.input_prompt()),
359 372 0))
360 373
361 374
362 375 def current_block_range(self):
363 376 return self.blockRanges.get(self.currentBlockID,
364 377 self.new_cell_block())
365 378
366 379 def current_block(self):
367 380 """The current block's text"""
368 381
369 382 return self.text_for_range(self.current_block_range().inputRange)
370 383
371 384 def text_for_range(self, textRange):
372 385 """text_for_range"""
373 386
374 387 ts = self.textView.textStorage()
375 388 return ts.string().substringWithRange_(textRange)
376 389
377 390 def current_line(self):
378 391 block = self.text_for_range(self.current_block_range().inputRange)
379 392 block = block.split('\n')
380 393 return block[-1]
381 394
382 395
383 396 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
384 397 """Insert text into textView at textRange, updating blockRanges
385 398 as necessary
386 399 """
387 400 if(textRange == None):
388 401 #range for end of text
389 402 textRange = NSMakeRange(self.textView.textStorage().length(), 0)
390 403
391 404
392 print textRange,string,self.textView.textStorage(),self.textView.textStorage().length()
393 405 self.textView.replaceCharactersInRange_withString_(
394 406 textRange, string)
395 407
396 408 for r in self.blockRanges.itervalues():
397 409 r.update_ranges_for_insertion(string, textRange)
398 410
399 411 self.textView.setSelectedRange_(textRange)
400 412 if(scrollToVisible):
401 413 self.textView.scrollRangeToVisible_(textRange)
402 414
403 415
404 416
405 417 def replace_current_block_with_string(self, textView, string):
406 418 textView.replaceCharactersInRange_withString_(
407 419 self.current_block_range().inputRange,
408 420 string)
409 421 self.current_block_range().inputRange.length = len(string)
410 422 r = NSMakeRange(textView.textStorage().length(), 0)
411 423 textView.scrollRangeToVisible_(r)
412 424 textView.setSelectedRange_(r)
413 425
414 426
415 427 def current_indent_string(self):
416 428 """returns string for indent or None if no indent"""
417 429
418 430 return self._indent_for_block(self.current_block())
419 431
420 432
421 433 def _indent_for_block(self, block):
422 434 lines = block.split('\n')
423 435 if(len(lines) > 1):
424 436 currentIndent = len(lines[-1]) - len(lines[-1].lstrip())
425 437 if(currentIndent == 0):
426 438 currentIndent = self.tabSpaces
427 439
428 440 if(self.tabUsesSpaces):
429 441 result = ' ' * currentIndent
430 442 else:
431 443 result = '\t' * (currentIndent/self.tabSpaces)
432 444 else:
433 445 result = None
434 446
435 447 return result
436 448
437 449
438 450 # NSTextView delegate methods...
439 451 def textView_doCommandBySelector_(self, textView, selector):
440 452 assert(textView == self.textView)
441 453 NSLog("textView_doCommandBySelector_: "+selector)
442 454
443 455
444 456 if(selector == 'insertNewline:'):
445 457 indent = self.current_indent_string()
446 458 if(indent):
447 459 line = indent + self.current_line()
448 460 else:
449 461 line = self.current_line()
450 462
451 463 if(self.is_complete(self.current_block())):
452 464 self.execute(self.current_block(),
453 465 blockID=self.currentBlockID)
454 466 self.start_new_block()
455 467
456 468 return True
457 469
458 470 return False
459 471
460 472 elif(selector == 'moveUp:'):
461 473 prevBlock = self.get_history_previous(self.current_block())
462 474 if(prevBlock != None):
463 475 self.replace_current_block_with_string(textView, prevBlock)
464 476 else:
465 477 NSBeep()
466 478 return True
467 479
468 480 elif(selector == 'moveDown:'):
469 481 nextBlock = self.get_history_next()
470 482 if(nextBlock != None):
471 483 self.replace_current_block_with_string(textView, nextBlock)
472 484 else:
473 485 NSBeep()
474 486 return True
475 487
476 488 elif(selector == 'moveToBeginningOfParagraph:'):
477 489 textView.setSelectedRange_(NSMakeRange(
478 490 self.current_block_range().inputRange.location,
479 491 0))
480 492 return True
481 493 elif(selector == 'moveToEndOfParagraph:'):
482 494 textView.setSelectedRange_(NSMakeRange(
483 495 self.current_block_range().inputRange.location + \
484 496 self.current_block_range().inputRange.length, 0))
485 497 return True
486 498 elif(selector == 'deleteToEndOfParagraph:'):
487 499 if(textView.selectedRange().location <= \
488 500 self.current_block_range().location):
489 501 raise NotImplemented()
490 502
491 503 return False # don't actually handle the delete
492 504
493 505 elif(selector == 'insertTab:'):
494 506 if(len(self.current_line().strip()) == 0): #only white space
495 507 return False
496 508 else:
497 509 self.textView.complete_(self)
498 510 return True
499 511
500 512 elif(selector == 'deleteBackward:'):
501 513 #if we're at the beginning of the current block, ignore
502 514 if(textView.selectedRange().location == \
503 515 self.current_block_range().inputRange.location):
504 516 return True
505 517 else:
506 518 for r in self.blockRanges.itervalues():
507 519 deleteRange = textView.selectedRange
508 520 if(deleteRange.length == 0):
509 521 deleteRange.location -= 1
510 522 deleteRange.length = 1
511 523 r.update_ranges_for_deletion(deleteRange)
512 524 return False
513 525 return False
514 526
515 527
516 528 def textView_shouldChangeTextInRanges_replacementStrings_(self,
517 529 textView, ranges, replacementStrings):
518 530 """
519 531 Delegate method for NSTextView.
520 532
521 533 Refuse change text in ranges not at end, but make those changes at
522 534 end.
523 535 """
524 536
525 537 assert(len(ranges) == len(replacementStrings))
526 538 allow = True
527 539 for r,s in zip(ranges, replacementStrings):
528 540 r = r.rangeValue()
529 541 if(textView.textStorage().length() > 0 and
530 542 r.location < self.current_block_range().inputRange.location):
531 self.insert_text(s, textRange=r)
543 self.insert_text(s)
532 544 allow = False
533 545
534 546 return allow
535 547
536 548 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self,
537 549 textView, words, charRange, index):
538 550 try:
539 551 ts = textView.textStorage()
540 552 token = ts.string().substringWithRange_(charRange)
541 553 completions = blockingCallFromThread(self.complete, token)
542 554 except:
543 555 completions = objc.nil
544 556 NSBeep()
545 557
546 558 return (completions,0)
547 559
548 560
General Comments 0
You need to be logged in to leave comments. Login now