-
Notifications
You must be signed in to change notification settings - Fork 237
/
console.nu
386 lines (341 loc) · 21.4 KB
/
console.nu
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
;; @file console.nu
;; @discussion An interactive Nu console in a Cocoa NSTextView.
;;
;; @copyright Copyright (c) 2007 Tim Burks, Radtastical Inc.
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
(global LPAREN '(')
(global RPAREN ')')
(load "nibtools") ;; dependency, needed to insert menu item
(function make-scrollview (frame block)
(let (v ((NSScrollView alloc) initWithFrame: frame))
(v set: (autoresizingMask: 18 hasHorizontalScroller: YES hasVerticalScroller: YES borderType: 2))
(block v)
v))
(class NSTextStorage
;; Search back from a closing paren to find its match.
(- (int) findOpeningParenForParenAt:(int) position backTo:(int) startOfInput is
(let ((count 0)
(index position)
(found NO)
(c nil))
(while (and (>= index startOfInput) (eq found NO))
(set c ((self string) characterAtIndex:index))
(case c
(LPAREN (set count (- count 1)))
(RPAREN (set count (+ count 1)))
(else nil))
(if (eq count 0)
(then (set found YES))
(else (set index (- index 1)))))
(if found (then index) (else -1))))
;; Search forward from an opening paren to find its match.
(- (int) findClosingParenForParenAt:(int) position is
(let ((count 0)
(index position)
(maxindex (self length))
(found NO)
(c nil))
(while (and (< index maxindex) (eq found NO))
(set c ((self string) characterAtIndex:index))
(case c
(LPAREN (set count (- count 1)))
(RPAREN (set count (+ count 1)))
(else nil))
(if (eq count 0)
(then (set found YES))
(else (set index (+ index 1)))))
(if found (then index) (else -1)))))
;; @abstract Value transformer class for binding to menu item.
;; @discussion This class is part of the Nu console implementation.
;; An instance of this class provides text to a menu item that toggles the display of the Nu console window.
(class NuConsoleShowHideTransformer is NSValueTransformer
;; Get the class of the transformed value (NSString).
(+ (Class) transformedValueClass is NSString)
;; Return NO because this transformer does not allow reverse transformation.
(+ (BOOL) allowsReverseTransformation is NO)
;; Convert a boolean value into an appropriate string for the menu item.
(- (id) transformedValue:(id) v is (if v (then "Hide Nu Console") (else "Show Nu Console"))))
(NSValueTransformer setValueTransformer: ((NuConsoleShowHideTransformer alloc) init)
forName:"NuConsoleShowHideTransformer")
;; @abstract Console window controller.
;; @discussion This class is part of the Nu console implementation.
;; This class controls a window that contains an interactive Nu console.
(class NuConsoleWindowController is NSWindowController
(ivar (id) parser (id) showConsole (id) alert (id) console (int) exitWhenClosed)
;; Setter to use to control the display of the console with bindings.
(- (void) setMyShowConsole:(id) showConsole is (self setValue:showConsole forKey:"showConsole"))
;; Toggle the console display.
(- (void) toggleConsole:(id) sender is
(if @showConsole
(then (self setMyShowConsole:NO) ((self window) close))
(else (self setMyShowConsole:YES) ((self window) makeKeyAndOrderFront:self))))
;; Add a menu item to toggle the console's display.
(- (void) addMenuItem is
(let (m ((NSMenuItem alloc) initWithTitle:"Toggle Nu Console" action:"toggleConsole:" keyEquivalent:"l"))
(m setTarget: self)
(m bind:"title" toObject:self withKeyPath:"showConsole"
options:(NSMutableDictionary dictionaryWithList:'("NSValueTransformerName" "NuConsoleShowHideTransformer")))
(let (windowMenu ((NSApplication sharedApplication) windowsMenu))
(if windowMenu (windowMenu insertItem:m atIndex:0)))))
;; Initialize a console.
(- (id) init is
(self initWithWindow:((NSPanel alloc) initWithContentRect:'(0 0 600 200)
styleMask:(+ NSTitledWindowMask NSClosableWindowMask NSMiniaturizableWindowMask NSResizableWindowMask NSUtilityWindowMask)
backing:NSBackingStoreBuffered
defer:NO))
(set @console ((NuConsoleViewController alloc) initWithFrame: (list 0 0 (- (((self window) frame) objectAtIndex:2) 17) (((self window) frame) objectAtIndex:3))))
(self setMyShowConsole:NO)
(self addMenuItem)
(set @exitWhenClosed NO)
(let (w (self window))
(w setContentView: (make-scrollview ((self window) frame)
(do (sv) (sv setDocumentView: (@console valueForKey:"textview")))))
(w center)
(w set: (title:"Nu Console" delegate:self opaque:NO hidesOnDeactivate:NO
frameOrigin: (NSValue valueWithPoint: (list (first ((self window) frame)) 80))
minSize: (NSValue valueWithSize: '(600 100))))
(if @showConsole
(w makeKeyAndOrderFront:self)))
(@console setFonts) ;; apparently this must be done AFTER the window is brought onscreen
(@console loadFile:"~/.nu")
self)
;; When a window resizes, move the cursor to the end of the input.
(- (void) windowDidResize: (id) notification is
(@console moveToEndOfInput))
;; On window close, optionally terminate the application.
(- (void) windowWillClose: (id) notification is
(if (eq @exitWhenClosed YES) ((NSApplication sharedApplication) terminate:self))
(self setMyShowConsole:NO))
;; When a window is to be closed, optionally threaten to terminate the application.
(- (BOOL) windowShouldClose: (id) sender is
(case @exitWhenClosed
(NO YES)
(YES (set @alert ((NSAlert alloc) init))
(let (a @alert)
(a setMessageText:"Do you really want to close this console? Your application will exit.")
(a setAlertStyle:NSCriticalAlertStyle)
(a addButtonWithTitle:"OK")
(a addButtonWithTitle:"Cancel")
(a beginSheetModalForWindow:(self window)
modalDelegate:self
didEndSelector:"alertDidEnd:returnCode:contextInfo:"
contextInfo:nil))
NO)))
;; Helper for window close alert.
(- (void) alertDidEnd:(id) alert returnCode:(int) code contextInfo:(void *) contextInfo is
(if (eq code 1000)
((self window) close))))
;; @abstract An NSTextView customization for Nu console display.
;; @discussion This class is part of the Nu console implementation.
;; This class provides special handling of key presses in the console.
(class NuConsoleView is NSTextView
;; Intercept key presses to capture control key sequences that enhance command line editing.
(- (void) keyDown: (id) event is
(cond ((eq 0 (& (event modifierFlags) NSControlKeyMask)) nil) ;; do nothing if control key is not pressed
((eq 0 (event keyCode)) ((self delegate) moveToStartOfInput)) ;; ctrl-a
((eq 14 (event keyCode)) ((self delegate) moveToEndOfInput)) ;; ctrl-e
(else nil))
(super keyDown: event)))
;; @abstract A controller for a text view containing a Nu console.
;; @discussion This class is part of the Nu console implementation.
;; It controls a Cocoa text view containing an interactive Nu console.
(class NuConsoleViewController is NSObject
(ivar (id) textview (id) startOfInput (id) insertionPoint (id) parser (id) history (id) index (id) count (id) chunk)
(- (id) loadFile:(id)file is
((_parser parse: (NSString stringWithContentsOfFile:
(file stringByResolvingSymlinksInPath)))
evalWithContext: (_parser context)))
;; Initialize a controller with a specified frame.
(- (id) initWithFrame:(NSRect) frame is
(super init)
(set @textview ((NuConsoleView alloc) initWithFrame: frame))
(@textview setAutoresizingMask: (+ NSViewHeightSizable NSViewWidthSizable))
(@textview set:
(backgroundColor: (NSColor colorWithDeviceRed:0.8 green:0.8 blue:1.0 alpha:0.9)
textColor: (NSColor blackColor)
insertionPointColor: (NSColor blackColor)
automaticQuoteSubstitutionEnabled: NO
delegate: self))
(set @startOfInput 0)
(set @insertionPoint 0)
(set @parser _parser) ;; _parser is a magic variable, automatically set in the context
(set $$console self)
(set @chunk 10)
(set @count 0)
(self prompt)
(set @history ((NSMutableArray alloc) init))
(set @index -1)
self)
;; Get the number of lines to output between handling application events.
(- (id) chunk is @chunk)
;; Set the number of lines to output between handling application events.
;; Setting this higher causes output to display faster, but more erratically.
(- (void) setChunk:(id) chunk is (set @chunk chunk))
;; Set the console font
(- (void) setFonts is
(@textview setFont: (NSFont fontWithName:"Monaco" size: 14)))
;; Get the console prompt.
(- (void) prompt is
;; In general, writes move both the insertionPoint and the startOfInput forward,
;; but we don't want to do this when we write the prompt.
(let ((savedInsertionPoint @insertionPoint))
(if (@parser incomplete)
(then (set @insertionPoint @startOfInput)
(self write: "- "))
(else (self write: "> ")))
(set @insertionPoint savedInsertionPoint)))
;; Write text to the console.
(- (void) write: (id) string is
((@textview textStorage) replaceCharactersInRange:(list @insertionPoint 0) withString:string)
(set @insertionPoint (+ @insertionPoint (string length)))
(set @startOfInput (+ @startOfInput (string length)))
(@textview scrollRangeToVisible:(list (self lengthOfTextView) 0))
(unless (NuMath integerMod:(set @count (+ @count 1)) by:@chunk)
((NSRunLoop currentRunLoop) runUntilDate:(NSDate date)))
(self moveToEndOfInput))
;; Move the console display to a specified point.
(- (void) moveAndScrollToIndex: (id) index is
(@textview scrollRangeToVisible:(list index 0))
(@textview setSelectedRange:(list index 0)))
;; Move the console display and cursor to the beginning of the input area.
(- (void) moveToStartOfInput is
(self moveAndScrollToIndex:@startOfInput))
;; Move the console display and cursor to the end of the input area.
(- (void) moveToEndOfInput is
(self moveAndScrollToIndex:(self lengthOfTextView)))
;; Get the length of the text view containing the console.
(- (id) lengthOfTextView is
(((@textview textStorage) mutableString) length))
;; Get the current line of input to the console.
(- (id) currentLine is
(let (text ((@textview textStorage) mutableString))
(text substringWithRange:(list @startOfInput (- (text length) @startOfInput)))))
;; Replace the current line of input with a line from the input history.
(- (void) replaceLineWithPrevious is
(cond ((eq @index 0) nil)
((eq @index -1) nil)
(else
(set @index (- @index 1))
((@textview textStorage)
replaceCharactersInRange:(list @startOfInput (- (self lengthOfTextView) @startOfInput))
withString:(@history objectAtIndex: @index))
(@textview scrollRangeToVisible:(list (self lengthOfTextView) 0)))))
;; Replace the current line of input with a line from the input history.
(- (void) replaceLineWithNext is
(cond ((eq @index (- (@history count) 0)) nil)
((eq @index (- (@history count) 1))
(set @index (+ @index 1))
((@textview textStorage)
replaceCharactersInRange:(list @startOfInput (- (self lengthOfTextView) @startOfInput))
withString:"")
(@textview scrollRangeToVisible:(list (self lengthOfTextView) 0)))
(else
(set @index (+ @index 1))
((@textview textStorage)
replaceCharactersInRange:(list @startOfInput (- (self lengthOfTextView) @startOfInput))
withString:(@history objectAtIndex: @index))
(@textview scrollRangeToVisible:(list (self lengthOfTextView) 0)))))
;; Delegate methods to handle text changes.
(- (BOOL) textView:(id) textview shouldChangeTextInRange:(NSRange) range replacementString:(id) replacement is
((@textview layoutManager) removeTemporaryAttribute:"NSColor" forCharacterRange:(list 0 (self lengthOfTextView)))
((@textview layoutManager) removeTemporaryAttribute:"NSBackgroundColor" forCharacterRange:(list 0 (self lengthOfTextView)))
((@textview layoutManager) removeTemporaryAttribute:"NSFont" forCharacterRange:(list 0 (self lengthOfTextView)))
(cond ((< (first range) @startOfInput) nil) ;; no edits are allowed before the prompt
((and (> (replacement length) 0) (eq (replacement characterAtIndex:(- (replacement length) 1)) RPAREN))
;; add the paren to the view
((@textview textStorage) replaceCharactersInRange:range withString:replacement)
;; look back for the opening paren so the pair can be highlighted.
(set match ((@textview textStorage)
findOpeningParenForParenAt:(first range)
backTo:(if (@parser incomplete) (then 0) (else @startOfInput))))
(cond ((and (eq match -1) (eq (@parser incomplete) 0))
;; let's try inserting a paren at the start of the line
((@textview textStorage) replaceCharactersInRange:(list @startOfInput 0) withString:"(")
(set highlight (NSMutableDictionary dictionaryWithList:
(list "NSColor" (NSColor colorWithDeviceRed:0.0 green:0.0 blue:0 alpha:1)
"NSBackgroundColor" (NSColor colorWithDeviceRed:0.9 green:0.9 blue:0 alpha:1))))
((@textview layoutManager) setTemporaryAttributes:highlight forCharacterRange:(list @startOfInput 1))
((@textview layoutManager) setTemporaryAttributes:highlight forCharacterRange:(list (+ (first range) 1) 1)))
; this code just highlights the unmatched paren in red without trying to fix anything
;(set highlight (NSMutableDictionary dictionaryWithList:
; (list "NSColor" ((NSColor redColor) colorWithAlphaComponent:1.0))))
;((@textview layoutManager) setTemporaryAttributes:highlight forCharacterRange:(list (first range) 1)))
(else
(set highlight (NSMutableDictionary dictionaryWithList:
(list "NSColor" (NSColor colorWithDeviceRed:0.0 green:0.0 blue:0 alpha:1)
"NSBackgroundColor" (NSColor colorWithDeviceRed:0.9 green:0.9 blue:0 alpha:1))))
((@textview layoutManager) setTemporaryAttributes:highlight forCharacterRange:(list (first range) 1))
((@textview layoutManager) setTemporaryAttributes:highlight forCharacterRange:(list match 1)))))
((and (> (replacement length) 0) (eq (replacement characterAtIndex:(- (replacement length) 1)) LPAREN))
;; add the paren to the view
((@textview textStorage) replaceCharactersInRange:range withString:replacement)
;; look back for the opening paren so the pair can be highlighted.
(set match ((@textview textStorage) findClosingParenForParenAt:(first range)))
(unless (eq match -1)
(set highlight (NSMutableDictionary dictionaryWithList:
(list "NSColor" (NSColor colorWithDeviceRed:0.0 green:0.0 blue:0 alpha:1)
"NSBackgroundColor" (NSColor colorWithDeviceRed:0.9 green:0.9 blue:0 alpha:1))))
((@textview layoutManager) setTemporaryAttributes:highlight forCharacterRange:(list (first range) 1))
((@textview layoutManager) setTemporaryAttributes:highlight forCharacterRange:(list match 1))))
((and (> (replacement length) 0) (eq (replacement characterAtIndex:(- (replacement length) 1)) 10))
((@textview textStorage) replaceCharactersInRange:(list (self lengthOfTextView) 0) withString:replacement)
(@textview setNeedsDisplay:1)
(set stringToEvaluate (self currentLine))
(set @startOfInput (self lengthOfTextView))
(if (> (stringToEvaluate length) 1)
(then
(@history addObject:(stringToEvaluate substringToIndex:(- (stringToEvaluate length) 1)))
(set @index (@history count))
(try
(set code (@parser parse: stringToEvaluate))
(unless (@parser incomplete)
(set @insertionPoint @startOfInput)
(set result (@parser eval: code))
(((@parser symbolTable) symbolWithString: "!!") setValue: result)
(if (send result respondsToSelector:"escapedStringRepresentation")
(then (set stringToDisplay (send result escapedStringRepresentation)))
(else (set stringToDisplay (send result stringValue))))
(self write:stringToDisplay)
(self write:"\n"))
(catch (exception)
;; don't use string interpolation here, it calls the parser again
(if (exception isKindOfClass:(NuException class))
(then (set result (exception dumpExcludingTopLevelCount:4))) ; leave off the console stack
(else (set result (exception dump))))
(self write:result)
;(self write:(exception name))
;(self write:": ")
;(self write:(exception reason))
;(self write:("\n"))
(@parser reset)
(set @insertionPoint @startOfInput))))
(else
(set @insertionPoint @startOfInput)))
(self prompt)
nil) ;; don't insert replacement text because we've already inserted it
(else YES))) ;; in the general case, the caller should insert replacement text
;; Delegate method to approve text changes.
(- (NSRange) textView:(id) textview
willChangeSelectionFromCharacterRange:(NSRange) oldRange
toCharacterRange:(NSRange) newRange is
(if (and (eq (newRange second) 0)
(< (first newRange) @startOfInput))
(then oldRange)
(else newRange)))
;; Delegate method to perform actions.
(- (int) textView:(id) textview doCommandBySelector:(SEL) selector is
(case selector
("moveUp:" (self replaceLineWithPrevious))
("moveDown:" (self replaceLineWithNext))
(else nil))))