##// END OF EJS Templates
fixed frontendbase.FrontEndBase.is_complete
Barry Wark -
Show More
@@ -1,369 +1,370 b''
1 1 # encoding: utf-8
2 2 # -*- test-case-name: ipython1.frontend.cocoa.tests.test_cocoa_frontend -*-
3 3
4 4 """PyObjC classes to provide a Cocoa frontend to the ipython1.kernel.engineservice.EngineService.
5 5
6 6 The Cocoa frontend is divided into two classes:
7 7 - IPythonCocoaController
8 8 - IPythonCLITextViewDelegate
9 9
10 10 To add an IPython interpreter to a cocoa app, instantiate both of these classes in an XIB...[FINISH]
11 11 """
12 12
13 13 __docformat__ = "restructuredtext en"
14 14
15 15 #-------------------------------------------------------------------------------
16 16 # Copyright (C) 2008 Barry Wark <barrywark@gmail.com>
17 17 #
18 18 # Distributed under the terms of the BSD License. The full license is in
19 19 # the file COPYING, distributed as part of this software.
20 20 #-------------------------------------------------------------------------------
21 21
22 22 #-------------------------------------------------------------------------------
23 23 # Imports
24 24 #-------------------------------------------------------------------------------
25 25
26 26 import objc
27 27 import uuid
28 28
29 29 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
30 30 NSLog, NSNotificationCenter, NSMakeRange,\
31 31 NSLocalizedString, NSIntersectionRange
32 32 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
33 33 NSTextView, NSRulerView, NSVerticalRuler
34 34
35 35 from pprint import saferepr
36 36
37 import IPython
37 38 from IPython.kernel.engineservice import EngineService, ThreadedEngineService
38 39 from IPython.frontend.frontendbase import FrontEndBase
39 40
40 41 from twisted.internet.threads import blockingCallFromThread
41 42
42 43 #-------------------------------------------------------------------------------
43 44 # Classes to implement the Cocoa frontend
44 45 #-------------------------------------------------------------------------------
45 46
46 47 # TODO:
47 48 # 1. use MultiEngineClient and out-of-process engine rather than ThreadedEngineService?
48 49 # 2. integrate Xgrid launching of engines
49 50
50 51
51 52
52 53
53 54 class IPythonCocoaController(NSObject, FrontEndBase):
54 55 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
55 56 waitingForEngine = objc.ivar().bool()
56 57 textView = objc.IBOutlet()
57 58
58 59 def init(self):
59 60 self = super(IPythonCocoaController, self).init()
60 61 FrontEndBase.__init__(self, engine=ThreadedEngineService())
61 62 if(self != None):
62 63 self._common_init()
63 64
64 65 return self
65 66
66 67 def _common_init(self):
67 68 """_common_init"""
68 69
69 70 self.userNS = NSMutableDictionary.dictionary()
70 71 self.waitingForEngine = False
71 72
72 73 self.lines = {}
73 74 self.tabSpaces = 4
74 75 self.tabUsesSpaces = True
75 76 self.currentBlockID = self.nextBlockID()
76 77 self.blockRanges = {} # blockID=>NSRange
77 78
78 79
79 80 def awakeFromNib(self):
80 81 """awakeFromNib"""
81 82
82 83 self._common_init()
83 84
84 85 # Start the IPython engine
85 86 self.engine.startService()
86 87 NSLog('IPython engine started')
87 88
88 89 # Register for app termination
89 90 NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self,
90 91 'appWillTerminate:',
91 92 NSApplicationWillTerminateNotification,
92 93 None)
93 94
94 95 self.textView.setDelegate_(self)
95 96 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
96 97 self.verticalRulerView = NSRulerView.alloc().initWithScrollView_orientation_(
97 98 self.textView.enclosingScrollView(),
98 99 NSVerticalRuler)
99 100 self.verticalRulerView.setClientView_(self.textView)
100 101 self.startCLIForTextView()
101 102
102 103
103 104 def appWillTerminate_(self, notification):
104 105 """appWillTerminate"""
105 106
106 107 self.engine.stopService()
107 108
108 109
109 110 def complete(self, token):
110 111 """Complete token in engine's user_ns
111 112
112 113 Parameters
113 114 ----------
114 115 token : string
115 116
116 117 Result
117 118 ------
118 119 Deferred result of ipython1.kernel.engineservice.IEngineInteractive.complete
119 120 """
120 121
121 122 return self.engine.complete(token)
122 123
123 124
124 125 def execute(self, block, blockID=None):
125 126 self.waitingForEngine = True
126 127 self.willChangeValueForKey_('commandHistory')
127 128 d = super(IPythonCocoaController, self).execute(block, blockID)
128 129 d.addBoth(self._engineDone)
129 130 d.addCallback(self._updateUserNS)
130 131
131 132 return d
132 133
133 134
134 135 def _engineDone(self, x):
135 136 self.waitingForEngine = False
136 137 self.didChangeValueForKey_('commandHistory')
137 138 return x
138 139
139 140 def _updateUserNS(self, result):
140 141 """Update self.userNS from self.engine's namespace"""
141 142 d = self.engine.keys()
142 143 d.addCallback(self._getEngineNamepsaceValuesForKeys)
143 144
144 145 return result
145 146
146 147
147 148 def _getEngineNamepsaceValuesForKeys(self, keys):
148 149 d = self.engine.pull(keys)
149 150 d.addCallback(self._storeEngineNamespaceValues, keys=keys)
150 151
151 152
152 153 def _storeEngineNamespaceValues(self, values, keys=[]):
153 154 assert(len(values) == len(keys))
154 155 self.willChangeValueForKey_('userNS')
155 156 for (k,v) in zip(keys,values):
156 157 self.userNS[k] = saferepr(v)
157 158 self.didChangeValueForKey_('userNS')
158 159
159 160
160 161 def startCLIForTextView(self):
161 162 """Print banner"""
162 163
163 banner = """IPython1 0.X -- An enhanced Interactive Python."""
164 banner = """IPython1 %s -- An enhanced Interactive Python.""" % IPython.__version__
164 165
165 166 self.insert_text(banner + '\n\n')
166 167
167 168 # NSTextView/IPythonTextView delegate methods
168 169 def textView_doCommandBySelector_(self, textView, selector):
169 170 assert(textView == self.textView)
170 171 NSLog("textView_doCommandBySelector_: "+selector)
171 172
172 173
173 174 if(selector == 'insertNewline:'):
174 175 indent = self.currentIndentString()
175 176 if(indent):
176 177 line = indent + self.currentLine()
177 178 else:
178 179 line = self.currentLine()
179 180
180 181 if(self.is_complete(self.currentBlock())):
181 182 self.execute(self.currentBlock(),
182 183 blockID=self.currentBlockID)
183 184 self.currentBlockID = self.nextBlockID()
184 185 return True
185 186
186 187 return False
187 188
188 189 elif(selector == 'moveUp:'):
189 190 self.replaceCurrentBlockWithString(textView, self.get_history_item_previous(self.currentBlock()))
190 191 return True
191 192
192 193 elif(selector == 'moveDown:'):
193 194 self.replaceCurrentBlockWithString(textView, self.get_history_item_next(self.currentBlock()))
194 195 return True
195 196
196 197 elif(selector == 'moveToBeginningOfParagraph:'):
197 198 textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location, 0))
198 199 return True
199 200 elif(selector == 'moveToEndOfParagraph:'):
200 201 textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location + \
201 202 self.currentBlockRange().length, 0))
202 203 return True
203 204 elif(selector == 'deleteToEndOfParagraph:'):
204 205 if(textView.selectedRange().location <= self.currentBlockRange().location):
205 206 # Intersect the selected range with the current line range
206 207 if(self.currentBlockRange().length < 0):
207 208 self.blockRanges[self.currentBlockID].length = 0
208 209
209 210 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
210 211 self.currentBlockRange())
211 212
212 213 if(r.length > 0): #no intersection
213 214 textView.setSelectedRange_(r)
214 215
215 216 return False # don't actually handle the delete
216 217
217 218 elif(selector == 'insertTab:'):
218 219 if(len(self.currentLine().strip()) == 0): #only white space
219 220 return False
220 221 else:
221 222 self.textView.complete_(self)
222 223 return True
223 224
224 225 elif(selector == 'deleteBackward:'):
225 226 #if we're at the beginning of the current block, ignore
226 227 if(textView.selectedRange().location == self.currentBlockRange().location):
227 228 return True
228 229 else:
229 230 self.currentBlockRange().length-=1
230 231 return False
231 232 return False
232 233
233 234
234 235 def textView_shouldChangeTextInRanges_replacementStrings_(self, textView, ranges, replacementStrings):
235 236 """
236 237 Delegate method for NSTextView.
237 238
238 239 Refuse change text in ranges not at end, but make those changes at end.
239 240 """
240 241
241 242 #print 'textView_shouldChangeTextInRanges_replacementStrings_:',ranges,replacementStrings
242 243 assert(len(ranges) == len(replacementStrings))
243 244 allow = True
244 245 for r,s in zip(ranges, replacementStrings):
245 246 r = r.rangeValue()
246 247 if(textView.textStorage().length() > 0 and
247 248 r.location < self.currentBlockRange().location):
248 249 self.insert_text(s)
249 250 allow = False
250 251
251 252
252 253 self.blockRanges.setdefault(self.currentBlockID, self.currentBlockRange()).length += len(s)
253 254
254 255 return allow
255 256
256 257 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, textView, words, charRange, index):
257 258 try:
258 259 token = textView.textStorage().string().substringWithRange_(charRange)
259 260 completions = blockingCallFromThread(self.complete, token)
260 261 except:
261 262 completions = objc.nil
262 263 NSBeep()
263 264
264 265 return (completions,0)
265 266
266 267
267 268 def nextBlockID(self):
268 269
269 270 return uuid.uuid4()
270 271
271 272 def currentBlockRange(self):
272 273 return self.blockRanges.get(self.currentBlockID, NSMakeRange(self.textView.textStorage().length(), 0))
273 274
274 275 def currentBlock(self):
275 276 """The current block's text"""
276 277
277 278 return self.textForRange(self.currentBlockRange())
278 279
279 280 def textForRange(self, textRange):
280 281 """textForRange"""
281 282
282 283 return self.textView.textStorage().string().substringWithRange_(textRange)
283 284
284 285 def currentLine(self):
285 286 block = self.textForRange(self.currentBlockRange())
286 287 block = block.split('\n')
287 288 return block[-1]
288 289
289 290 def update_cell_prompt(self, result):
290 291 blockID = result['blockID']
291 292 self.insert_text(self.inputPrompt(result=result),
292 293 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
293 294 scrollToVisible=False
294 295 )
295 296
296 297 return result
297 298
298 299
299 300 def render_result(self, result):
300 301 blockID = result['blockID']
301 302 inputRange = self.blockRanges[blockID]
302 303 del self.blockRanges[blockID]
303 304
304 305 #print inputRange,self.currentBlockRange()
305 306 self.insert_text('\n' +
306 307 self.outputPrompt(result) +
307 308 result.get('display',{}).get('pprint','') +
308 309 '\n\n',
309 310 textRange=NSMakeRange(inputRange.location+inputRange.length, 0))
310 311 return result
311 312
312 313
313 314 def render_error(self, failure):
314 315 self.insert_text(str(failure))
315 316 return failure
316 317
317 318
318 319 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
319 320 """Insert text into textView at textRange, updating blockRanges as necessary"""
320 321
321 322 if(textRange == None):
322 323 textRange = NSMakeRange(self.textView.textStorage().length(), 0) #range for end of text
323 324
324 325 for r in self.blockRanges.itervalues():
325 326 intersection = NSIntersectionRange(r,textRange)
326 327 if(intersection.length == 0): #ranges don't intersect
327 328 if r.location >= textRange.location:
328 329 r.location += len(string)
329 330 else: #ranges intersect
330 331 if(r.location <= textRange.location):
331 332 assert(intersection.length == textRange.length)
332 333 r.length += textRange.length
333 334 else:
334 335 r.location += intersection.length
335 336
336 337 self.textView.replaceCharactersInRange_withString_(textRange, string) #textStorage().string()
337 338 self.textView.setSelectedRange_(NSMakeRange(textRange.location+len(string), 0))
338 339 if(scrollToVisible):
339 340 self.textView.scrollRangeToVisible_(textRange)
340 341
341 342
342 343
343 344 def replaceCurrentBlockWithString(self, textView, string):
344 345 textView.replaceCharactersInRange_withString_(self.currentBlockRange(),
345 346 string)
346 347 r = NSMakeRange(textView.textStorage().length(), 0)
347 348 textView.scrollRangeToVisible_(r)
348 349 textView.setSelectedRange_(r)
349 350
350 351
351 352 def currentIndentString(self):
352 353 """returns string for indent or None if no indent"""
353 354
354 355 if(len(self.currentBlock()) > 0):
355 356 lines = self.currentBlock().split('\n')
356 357 currentIndent = len(lines[-1]) - len(lines[-1])
357 358 if(currentIndent == 0):
358 359 currentIndent = self.tabSpaces
359 360
360 361 if(self.tabUsesSpaces):
361 362 result = ' ' * currentIndent
362 363 else:
363 364 result = '\t' * (currentIndent/self.tabSpaces)
364 365 else:
365 366 result = None
366 367
367 368 return result
368 369
369 370
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
@@ -1,295 +1,297 b''
1 1 # encoding: utf-8
2 2 """
3 3 frontendbase provides an interface and base class for GUI frontends for IPython.kernel/IPython.kernel.core.
4 4
5 5 Frontend implementations will likely want to subclass FrontEndBase.
6 6
7 7 Author: Barry Wark
8 8 """
9 9 __docformat__ = "restructuredtext en"
10 10
11 11 #-------------------------------------------------------------------------------
12 12 # Copyright (C) 2008 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is in
15 15 # the file COPYING, distributed as part of this software.
16 16 #-------------------------------------------------------------------------------
17 17
18 18 #-------------------------------------------------------------------------------
19 19 # Imports
20 20 #-------------------------------------------------------------------------------
21 21 import string
22 22 import uuid
23 23
24 24
25 25 from IPython.kernel.core.history import FrontEndHistory
26 26 from IPython.kernel.core.util import Bunch
27 27
28 28 from IPython.kernel.engineservice import IEngineCore
29 29
30 30 import zope.interface as zi
31 31
32 32 import _ast
33 33
34 34 ##############################################################################
35 35 # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
36 36 # not
37 37
38 38 rc = Bunch()
39 39 rc.prompt_in1 = r'In [$number]: '
40 40 rc.prompt_in2 = r'...'
41 41 rc.prompt_out = r'Out [$number]: '
42 42
43 43 ##############################################################################
44 44
45 45 class IFrontEndFactory(zi.Interface):
46 46 """Factory interface for frontends."""
47 47
48 48 def __call__(engine=None, history=None):
49 49 """
50 50 Parameters:
51 51 interpreter : IPython.kernel.engineservice.IEngineCore
52 52 """
53 53
54 54 pass
55 55
56 56
57 57
58 58 class IFrontEnd(zi.Interface):
59 59 """Interface for frontends. All methods return t.i.d.Deferred"""
60 60
61 61 zi.Attribute("input_prompt_template", "string.Template instance substituteable with execute result.")
62 62 zi.Attribute("output_prompt_template", "string.Template instance substituteable with execute result.")
63 63 zi.Attribute("continuation_prompt_template", "string.Template instance substituteable with execute result.")
64 64
65 65 def update_cell_prompt(self, result):
66 66 """Subclass may override to update the input prompt for a block.
67 67 Since this method will be called as a twisted.internet.defer.Deferred's callback,
68 68 implementations should return result when finished."""
69 69
70 70 pass
71 71
72 72 def render_result(self, result):
73 73 """Render the result of an execute call. Implementors may choose the method of rendering.
74 74 For example, a notebook-style frontend might render a Chaco plot inline.
75 75
76 76 Parameters:
77 77 result : dict (result of IEngineBase.execute )
78 78
79 79 Result:
80 80 Output of frontend rendering
81 81 """
82 82
83 83 pass
84 84
85 85 def render_error(self, failure):
86 86 """Subclasses must override to render the failure. Since this method will be called as a
87 87 twisted.internet.defer.Deferred's callback, implementations should return result
88 88 when finished."""
89 89
90 90 pass
91 91
92 92
93 93 def inputPrompt(result={}):
94 94 """Returns the input prompt by subsituting into self.input_prompt_template"""
95 95 pass
96 96
97 97 def outputPrompt(result):
98 98 """Returns the output prompt by subsituting into self.output_prompt_template"""
99 99
100 100 pass
101 101
102 102 def continuationPrompt():
103 103 """Returns the continuation prompt by subsituting into self.continuation_prompt_template"""
104 104
105 105 pass
106 106
107 107 def is_complete(block):
108 108 """Returns True if block is complete, False otherwise."""
109 109
110 110 pass
111 111
112 112 def compile_ast(block):
113 113 """Compiles block to an _ast.AST"""
114 114
115 115 pass
116 116
117 117
118 118 def get_history_item_previous(currentBlock):
119 119 """Returns the block previous in the history."""
120 120 pass
121 121
122 122 def get_history_item_next(currentBlock):
123 123 """Returns the next block in the history."""
124 124
125 125 pass
126 126
127 127
128 128 class FrontEndBase(object):
129 129 """
130 130 FrontEndBase manages the state tasks for a CLI frontend:
131 131 - Input and output history management
132 132 - Input/continuation and output prompt generation
133 133
134 134 Some issues (due to possibly unavailable engine):
135 135 - How do we get the current cell number for the engine?
136 136 - How do we handle completions?
137 137 """
138 138
139 139 zi.implements(IFrontEnd)
140 140 zi.classProvides(IFrontEndFactory)
141 141
142 142 history_cursor = 0
143 143
144 144 current_indent_level = 0
145 145
146 146
147 147 input_prompt_template = string.Template(rc.prompt_in1)
148 148 output_prompt_template = string.Template(rc.prompt_out)
149 149 continuation_prompt_template = string.Template(rc.prompt_in2)
150 150
151 151 def __init__(self, engine=None, history=None):
152 152 assert(engine==None or IEngineCore.providedBy(engine))
153 153 self.engine = IEngineCore(engine)
154 154 if history is None:
155 155 self.history = FrontEndHistory(input_cache=[''])
156 156 else:
157 157 self.history = history
158 158
159 159
160 160 def inputPrompt(self, result={}):
161 161 """Returns the current input prompt
162 162
163 163 It would be great to use ipython1.core.prompts.Prompt1 here
164 164 """
165 165
166 166 result.setdefault('number','')
167 167
168 168 return self.input_prompt_template.safe_substitute(result)
169 169
170 170
171 171 def continuationPrompt(self):
172 172 """Returns the current continuation prompt"""
173 173
174 174 return self.continuation_prompt_template.safe_substitute()
175 175
176 176 def outputPrompt(self, result):
177 177 """Returns the output prompt for result"""
178 178
179 179 return self.output_prompt_template.safe_substitute(result)
180 180
181 181
182 182 def is_complete(self, block):
183 183 """Determine if block is complete.
184 184
185 185 Parameters
186 186 block : string
187 187
188 188 Result
189 189 True if block can be sent to the engine without compile errors.
190 190 False otherwise.
191 191 """
192 192
193 193 try:
194 self.compile_ast(block)
195 return True
194 ast = self.compile_ast(block)
196 195 except:
197 196 return False
197
198 lines = block.split('\n')
199 return (len(lines)==1 or str(lines[-1])=='')
198 200
199 201
200 202 def compile_ast(self, block):
201 203 """Compile block to an AST
202 204
203 205 Parameters:
204 206 block : str
205 207
206 208 Result:
207 209 AST
208 210
209 211 Throws:
210 212 Exception if block cannot be compiled
211 213 """
212 214
213 215 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
214 216
215 217
216 218 def execute(self, block, blockID=None):
217 219 """Execute the block and return result.
218 220
219 221 Parameters:
220 222 block : {str, AST}
221 223 blockID : any
222 224 Caller may provide an ID to identify this block. result['blockID'] := blockID
223 225
224 226 Result:
225 227 Deferred result of self.interpreter.execute
226 228 """
227 229 # if(not isinstance(block, _ast.AST)):
228 230 # block = self.compile_ast(block)
229 231
230 232 if(blockID == None):
231 233 blockID = uuid.uuid4() #random UUID
232 234
233 235 d = self.engine.execute(block)
234 236 d.addCallback(self._add_block_id, blockID)
235 237 d.addCallback(self.update_cell_prompt)
236 238 d.addCallbacks(self.render_result, errback=self.render_error)
237 239
238 240 return d
239 241
240 242 def _add_block_id(self, result, blockID):
241 243 """add_block_id"""
242 244
243 245 result['blockID'] = blockID
244 246
245 247 return result
246 248
247 249
248 250 def get_history_item_previous(self, currentBlock):
249 251 """ Returns previous history string and decrement history cursor.
250 252 """
251 253 command = self.history.get_history_item(self.history_cursor - 1)
252 254 if command is not None:
253 255 self.history.input_cache[self.history_cursor] = currentBlock
254 256 self.history_cursor -= 1
255 257 return command
256 258
257 259
258 260 def get_history_item_next(self, currentBlock):
259 261 """ Returns next history string and increment history cursor.
260 262 """
261 263 command = self.history.get_history_item(self.history_cursor + 1)
262 264 if command is not None:
263 265 self.history.input_cache[self.history_cursor] = currentBlock
264 266 self.history_cursor += 1
265 267 return command
266 268
267 269 ###
268 270 # Subclasses probably want to override these methods...
269 271 ###
270 272
271 273 def update_cell_prompt(self, result):
272 274 """Subclass may override to update the input prompt for a block.
273 275 Since this method will be called as a twisted.internet.defer.Deferred's callback,
274 276 implementations should return result when finished."""
275 277
276 278 return result
277 279
278 280
279 281 def render_result(self, result):
280 282 """Subclasses must override to render result. Since this method will be called as a
281 283 twisted.internet.defer.Deferred's callback, implementations should return result
282 284 when finished."""
283 285
284 286 return result
285 287
286 288
287 289 def render_error(self, failure):
288 290 """Subclasses must override to render the failure. Since this method will be called as a
289 291 twisted.internet.defer.Deferred's callback, implementations should return result
290 292 when finished."""
291 293
292 294 return failure
293 295
294 296
295 297
General Comments 0
You need to be logged in to leave comments. Login now