##// END OF EJS Templates
fix for cocoa frontend current_indent_string; refactor to make testing easier and added a test for _indent_for_block
Barry Wark -
Show More
@@ -1,425 +1,429 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 objc
28 28 import uuid
29 29
30 30 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
31 31 NSLog, NSNotificationCenter, NSMakeRange,\
32 32 NSLocalizedString, NSIntersectionRange
33 33
34 34 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
35 35 NSTextView, NSRulerView, NSVerticalRuler
36 36
37 37 from pprint import saferepr
38 38
39 39 import IPython
40 40 from IPython.kernel.engineservice import ThreadedEngineService
41 41 from IPython.frontend.frontendbase import FrontEndBase
42 42
43 43 from twisted.internet.threads import blockingCallFromThread
44 44 from twisted.python.failure import Failure
45 45
46 46 #------------------------------------------------------------------------------
47 47 # Classes to implement the Cocoa frontend
48 48 #------------------------------------------------------------------------------
49 49
50 50 # TODO:
51 51 # 1. use MultiEngineClient and out-of-process engine rather than
52 52 # ThreadedEngineService?
53 53 # 2. integrate Xgrid launching of engines
54 54
55 55
56 56
57 57
58 58 class IPythonCocoaController(NSObject, FrontEndBase):
59 59 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
60 60 waitingForEngine = objc.ivar().bool()
61 61 textView = objc.IBOutlet()
62 62
63 63 def init(self):
64 64 self = super(IPythonCocoaController, self).init()
65 65 FrontEndBase.__init__(self, engine=ThreadedEngineService())
66 66 if(self != None):
67 67 self._common_init()
68 68
69 69 return self
70 70
71 71 def _common_init(self):
72 72 """_common_init"""
73 73
74 74 self.userNS = NSMutableDictionary.dictionary()
75 75 self.waitingForEngine = False
76 76
77 77 self.lines = {}
78 78 self.tabSpaces = 4
79 79 self.tabUsesSpaces = True
80 80 self.currentBlockID = self.next_block_ID()
81 81 self.blockRanges = {} # blockID=>NSRange
82 82
83 83
84 84 def awakeFromNib(self):
85 85 """awakeFromNib"""
86 86
87 87 self._common_init()
88 88
89 89 # Start the IPython engine
90 90 self.engine.startService()
91 91 NSLog('IPython engine started')
92 92
93 93 # Register for app termination
94 94 nc = NSNotificationCenter.defaultCenter()
95 95 nc.addObserver_selector_name_object_(
96 96 self,
97 97 'appWillTerminate:',
98 98 NSApplicationWillTerminateNotification,
99 99 None)
100 100
101 101 self.textView.setDelegate_(self)
102 102 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
103 103 r = NSRulerView.alloc().initWithScrollView_orientation_(
104 104 self.textView.enclosingScrollView(),
105 105 NSVerticalRuler)
106 106 self.verticalRulerView = r
107 107 self.verticalRulerView.setClientView_(self.textView)
108 108 self._start_cli_banner()
109 109
110 110
111 111 def appWillTerminate_(self, notification):
112 112 """appWillTerminate"""
113 113
114 114 self.engine.stopService()
115 115
116 116
117 117 def complete(self, token):
118 118 """Complete token in engine's user_ns
119 119
120 120 Parameters
121 121 ----------
122 122 token : string
123 123
124 124 Result
125 125 ------
126 126 Deferred result of
127 127 IPython.kernel.engineservice.IEngineBase.complete
128 128 """
129 129
130 130 return self.engine.complete(token)
131 131
132 132
133 133 def execute(self, block, blockID=None):
134 134 self.waitingForEngine = True
135 135 self.willChangeValueForKey_('commandHistory')
136 136 d = super(IPythonCocoaController, self).execute(block, blockID)
137 137 d.addBoth(self._engine_done)
138 138 d.addCallback(self._update_user_ns)
139 139
140 140 return d
141 141
142 142
143 143 def _engine_done(self, x):
144 144 self.waitingForEngine = False
145 145 self.didChangeValueForKey_('commandHistory')
146 146 return x
147 147
148 148 def _update_user_ns(self, result):
149 149 """Update self.userNS from self.engine's namespace"""
150 150 d = self.engine.keys()
151 151 d.addCallback(self._get_engine_namespace_values_for_keys)
152 152
153 153 return result
154 154
155 155
156 156 def _get_engine_namespace_values_for_keys(self, keys):
157 157 d = self.engine.pull(keys)
158 158 d.addCallback(self._store_engine_namespace_values, keys=keys)
159 159
160 160
161 161 def _store_engine_namespace_values(self, values, keys=[]):
162 162 assert(len(values) == len(keys))
163 163 self.willChangeValueForKey_('userNS')
164 164 for (k,v) in zip(keys,values):
165 165 self.userNS[k] = saferepr(v)
166 166 self.didChangeValueForKey_('userNS')
167 167
168 168
169 169 def update_cell_prompt(self, result):
170 170 if(isinstance(result, Failure)):
171 171 blockID = result.blockID
172 172 else:
173 173 blockID = result['blockID']
174 174
175 175
176 176 self.insert_text(self.input_prompt(result=result),
177 177 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
178 178 scrollToVisible=False
179 179 )
180 180
181 181 return result
182 182
183 183
184 184 def render_result(self, result):
185 185 blockID = result['blockID']
186 186 inputRange = self.blockRanges[blockID]
187 187 del self.blockRanges[blockID]
188 188
189 189 #print inputRange,self.current_block_range()
190 190 self.insert_text('\n' +
191 191 self.output_prompt(result) +
192 192 result.get('display',{}).get('pprint','') +
193 193 '\n\n',
194 194 textRange=NSMakeRange(inputRange.location+inputRange.length,
195 195 0))
196 196 return result
197 197
198 198
199 199 def render_error(self, failure):
200 200 self.insert_text('\n\n'+str(failure)+'\n\n')
201 201 self.start_new_block()
202 202 return failure
203 203
204 204
205 205 def _start_cli_banner(self):
206 206 """Print banner"""
207 207
208 208 banner = """IPython1 %s -- An enhanced Interactive Python.""" % \
209 209 IPython.__version__
210 210
211 211 self.insert_text(banner + '\n\n')
212 212
213 213
214 214 def start_new_block(self):
215 215 """"""
216 216
217 217 self.currentBlockID = self.next_block_ID()
218 218
219 219
220 220
221 221 def next_block_ID(self):
222 222
223 223 return uuid.uuid4()
224 224
225 225 def current_block_range(self):
226 226 return self.blockRanges.get(self.currentBlockID,
227 227 NSMakeRange(self.textView.textStorage().length(),
228 228 0))
229 229
230 230 def current_block(self):
231 231 """The current block's text"""
232 232
233 233 return self.text_for_range(self.current_block_range())
234 234
235 235 def text_for_range(self, textRange):
236 236 """text_for_range"""
237 237
238 238 ts = self.textView.textStorage()
239 239 return ts.string().substringWithRange_(textRange)
240 240
241 241 def current_line(self):
242 242 block = self.text_for_range(self.current_block_range())
243 243 block = block.split('\n')
244 244 return block[-1]
245 245
246 246
247 247 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
248 248 """Insert text into textView at textRange, updating blockRanges
249 249 as necessary
250 250 """
251 251
252 252 if(textRange == None):
253 253 #range for end of text
254 254 textRange = NSMakeRange(self.textView.textStorage().length(), 0)
255 255
256 256 for r in self.blockRanges.itervalues():
257 257 intersection = NSIntersectionRange(r,textRange)
258 258 if(intersection.length == 0): #ranges don't intersect
259 259 if r.location >= textRange.location:
260 260 r.location += len(string)
261 261 else: #ranges intersect
262 262 if(r.location <= textRange.location):
263 263 assert(intersection.length == textRange.length)
264 264 r.length += textRange.length
265 265 else:
266 266 r.location += intersection.length
267 267
268 268 self.textView.replaceCharactersInRange_withString_(
269 269 textRange, string)
270 270 self.textView.setSelectedRange_(
271 271 NSMakeRange(textRange.location+len(string), 0))
272 272 if(scrollToVisible):
273 273 self.textView.scrollRangeToVisible_(textRange)
274 274
275 275
276 276
277 277
278 278 def replace_current_block_with_string(self, textView, string):
279 279 textView.replaceCharactersInRange_withString_(
280 280 self.current_block_range(),
281 281 string)
282 282 self.current_block_range().length = len(string)
283 283 r = NSMakeRange(textView.textStorage().length(), 0)
284 284 textView.scrollRangeToVisible_(r)
285 285 textView.setSelectedRange_(r)
286 286
287 287
288 288 def current_indent_string(self):
289 289 """returns string for indent or None if no indent"""
290 290
291 if(len(self.current_block()) > 0):
292 lines = self.current_block().split('\n')
293 currentIndent = len(lines[-1]) - len(lines[-1])
291 return self._indent_for_block(self.currentBlock())
292
293
294 def _indent_for_block(self, block):
295 lines = block.split('\n')
296 if(len(lines) > 1):
297 currentIndent = len(lines[-1]) - len(lines[-1].lstrip())
294 298 if(currentIndent == 0):
295 299 currentIndent = self.tabSpaces
296 300
297 301 if(self.tabUsesSpaces):
298 302 result = ' ' * currentIndent
299 303 else:
300 304 result = '\t' * (currentIndent/self.tabSpaces)
301 305 else:
302 306 result = None
303 307
304 308 return result
305 309
306 310
307 311 # NSTextView delegate methods...
308 312 def textView_doCommandBySelector_(self, textView, selector):
309 313 assert(textView == self.textView)
310 314 NSLog("textView_doCommandBySelector_: "+selector)
311 315
312 316
313 317 if(selector == 'insertNewline:'):
314 318 indent = self.current_indent_string()
315 319 if(indent):
316 320 line = indent + self.current_line()
317 321 else:
318 322 line = self.current_line()
319 323
320 324 if(self.is_complete(self.current_block())):
321 325 self.execute(self.current_block(),
322 326 blockID=self.currentBlockID)
323 327 self.start_new_block()
324 328
325 329 return True
326 330
327 331 return False
328 332
329 333 elif(selector == 'moveUp:'):
330 334 prevBlock = self.get_history_previous(self.current_block())
331 335 if(prevBlock != None):
332 336 self.replace_current_block_with_string(textView, prevBlock)
333 337 else:
334 338 NSBeep()
335 339 return True
336 340
337 341 elif(selector == 'moveDown:'):
338 342 nextBlock = self.get_history_next()
339 343 if(nextBlock != None):
340 344 self.replace_current_block_with_string(textView, nextBlock)
341 345 else:
342 346 NSBeep()
343 347 return True
344 348
345 349 elif(selector == 'moveToBeginningOfParagraph:'):
346 350 textView.setSelectedRange_(NSMakeRange(
347 351 self.current_block_range().location,
348 352 0))
349 353 return True
350 354 elif(selector == 'moveToEndOfParagraph:'):
351 355 textView.setSelectedRange_(NSMakeRange(
352 356 self.current_block_range().location + \
353 357 self.current_block_range().length, 0))
354 358 return True
355 359 elif(selector == 'deleteToEndOfParagraph:'):
356 360 if(textView.selectedRange().location <= \
357 361 self.current_block_range().location):
358 362 # Intersect the selected range with the current line range
359 363 if(self.current_block_range().length < 0):
360 364 self.blockRanges[self.currentBlockID].length = 0
361 365
362 366 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
363 367 self.current_block_range())
364 368
365 369 if(r.length > 0): #no intersection
366 370 textView.setSelectedRange_(r)
367 371
368 372 return False # don't actually handle the delete
369 373
370 374 elif(selector == 'insertTab:'):
371 375 if(len(self.current_line().strip()) == 0): #only white space
372 376 return False
373 377 else:
374 378 self.textView.complete_(self)
375 379 return True
376 380
377 381 elif(selector == 'deleteBackward:'):
378 382 #if we're at the beginning of the current block, ignore
379 383 if(textView.selectedRange().location == \
380 384 self.current_block_range().location):
381 385 return True
382 386 else:
383 387 self.current_block_range().length-=1
384 388 return False
385 389 return False
386 390
387 391
388 392 def textView_shouldChangeTextInRanges_replacementStrings_(self,
389 393 textView, ranges, replacementStrings):
390 394 """
391 395 Delegate method for NSTextView.
392 396
393 397 Refuse change text in ranges not at end, but make those changes at
394 398 end.
395 399 """
396 400
397 401 assert(len(ranges) == len(replacementStrings))
398 402 allow = True
399 403 for r,s in zip(ranges, replacementStrings):
400 404 r = r.rangeValue()
401 405 if(textView.textStorage().length() > 0 and
402 406 r.location < self.current_block_range().location):
403 407 self.insert_text(s)
404 408 allow = False
405 409
406 410
407 411 self.blockRanges.setdefault(self.currentBlockID,
408 412 self.current_block_range()).length +=\
409 413 len(s)
410 414
411 415 return allow
412 416
413 417 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self,
414 418 textView, words, charRange, index):
415 419 try:
416 420 ts = textView.textStorage()
417 421 token = ts.string().substringWithRange_(charRange)
418 422 completions = blockingCallFromThread(self.complete, token)
419 423 except:
420 424 completions = objc.nil
421 425 NSBeep()
422 426
423 427 return (completions,0)
424 428
425 429
@@ -1,72 +1,91 b''
1 1 # encoding: utf-8
2 2 """This file contains unittests for the
3 3 IPython.frontend.cocoa.cocoa_frontend module.
4 4 """
5 5 __docformat__ = "restructuredtext en"
6 6
7 7 #---------------------------------------------------------------------------
8 8 # Copyright (C) 2005 The IPython Development Team
9 9 #
10 10 # Distributed under the terms of the BSD License. The full license is in
11 11 # the file COPYING, distributed as part of this software.
12 12 #---------------------------------------------------------------------------
13 13
14 14 #---------------------------------------------------------------------------
15 15 # Imports
16 16 #---------------------------------------------------------------------------
17 17 from IPython.kernel.core.interpreter import Interpreter
18 18 import IPython.kernel.engineservice as es
19 19 from IPython.testing.util import DeferredTestCase
20 20 from twisted.internet.defer import succeed
21 21 from IPython.frontend.cocoa.cocoa_frontend import IPythonCocoaController
22 22
23 23 from Foundation import NSMakeRect
24 24 from AppKit import NSTextView, NSScrollView
25 25
26 26 class TestIPythonCocoaControler(DeferredTestCase):
27 27 """Tests for IPythonCocoaController"""
28 28
29 29 def setUp(self):
30 30 self.controller = IPythonCocoaController.alloc().init()
31 31 self.engine = es.EngineService()
32 32 self.engine.startService()
33 33
34 34
35 35 def tearDown(self):
36 36 self.controller = None
37 37 self.engine.stopService()
38 38
39 39 def testControllerExecutesCode(self):
40 40 code ="""5+5"""
41 41 expected = Interpreter().execute(code)
42 42 del expected['number']
43 43 def removeNumberAndID(result):
44 44 del result['number']
45 45 del result['id']
46 46 return result
47 47 self.assertDeferredEquals(
48 48 self.controller.execute(code).addCallback(removeNumberAndID),
49 49 expected)
50 50
51 51 def testControllerMirrorsUserNSWithValuesAsStrings(self):
52 52 code = """userns1=1;userns2=2"""
53 53 def testControllerUserNS(result):
54 54 self.assertEquals(self.controller.userNS['userns1'], 1)
55 55 self.assertEquals(self.controller.userNS['userns2'], 2)
56 56
57 57 self.controller.execute(code).addCallback(testControllerUserNS)
58 58
59 59
60 60 def testControllerInstantiatesIEngine(self):
61 61 self.assert_(es.IEngineBase.providedBy(self.controller.engine))
62 62
63 63 def testControllerCompletesToken(self):
64 64 code = """longNameVariable=10"""
65 65 def testCompletes(result):
66 66 self.assert_("longNameVariable" in result)
67 67
68 68 def testCompleteToken(result):
69 69 self.controller.complete("longNa").addCallback(testCompletes)
70 70
71 71 self.controller.execute(code).addCallback(testCompletes)
72 72
73
74 def testCurrentIndent(self):
75 """test that current_indent_string returns current indent or None.
76 Uses _indent_for_block for direct unit testing.
77 """
78
79 self.controller.tabUsesSpaces = True
80 self.assert_(self.controller._indent_for_block("""a=3""") == None)
81 self.assert_(self.controller._indent_for_block("") == None)
82 block = """def test():\n a=3"""
83 self.assert_(self.controller._indent_for_block(block) == \
84 ' ' * self.controller.tabSpaces)
85
86 block = """if(True):\n%sif(False):\n%spass""" % \
87 (' '*self.controller.tabSpaces,
88 2*' '*self.controller.tabSpaces)
89 self.assert_(self.controller._indent_for_block(block) == \
90 2*(' '*self.controller.tabSpaces))
91
General Comments 0
You need to be logged in to leave comments. Login now