Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(218)

Side by Side Diff: tools/ddbg/lib/commando.dart

Issue 69343017: Add support for command-line editing to ddbg. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « tools/ddbg.dart ('k') | tools/ddbg/lib/terminfo.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 import 'dart:async';
6 import 'dart:convert';
7 import 'dart:io';
8 import 'dart:math';
9
10 import 'terminfo.dart';
11
12 typedef List<String> CommandCompleter(List<String> commandParts);
13
14 class Commando {
15 // Ctrl keys
16 static const runeCtrlA = 0x01;
17 static const runeCtrlB = 0x02;
18 static const runeCtrlD = 0x04;
19 static const runeCtrlE = 0x05;
20 static const runeCtrlF = 0x06;
21 static const runeTAB = 0x09;
22 static const runeNewline = 0x0a;
23 static const runeCtrlK = 0x0b;
24 static const runeCtrlL = 0x0c;
25 static const runeCtrlN = 0x0e;
26 static const runeCtrlP = 0x10;
27 static const runeCtrlU = 0x15;
28 static const runeCtrlY = 0x19;
29 static const runeESC = 0x1b;
30 static const runeSpace = 0x20;
31 static const runeDEL = 0x7F;
32
33 Commando(this._stdin,
34 this._stdout,
35 this._handleCommand,
36 {this.prompt : '> ', this.completer : null}) {
37 _stdin.echoMode = false;
38 _stdin.lineMode = false;
39 _screenWidth = _term.cols - 1;
40 _writePrompt();
41 _stdinSubscription =
42 _stdin.transform(UTF8.decoder).listen(_handleText, onDone:done);
43 }
44
45 void _handleText(String text) {
46 try {
47 if (!_promptShown) {
48 _bufferedInput.write(text);
49 return;
50 }
51
52 var runes = text.runes.toList();
53 var pos = 0;
54 while (pos < runes.length) {
55 if (!_promptShown) {
56 // A command was processed which hid the prompt. Buffer
57 // the rest of the input.
58 //
59 // TODO(turnidge): Here and elsewhere in the file I pass
60 // runes to String.fromCharCodes. Does this work?
61 _bufferedInput.write(
62 new String.fromCharCodes(runes.skip(pos)));
63 return;
64 }
65
66 var rune = runes[pos];
67
68 // Count consecutive tabs because double-tab is meaningful.
69 if (rune == runeTAB) {
70 _tabCount++;
71 } else {
72 _tabCount = 0;
73 }
74
75 if (_isControlRune(rune)) {
76 pos += _handleControlSequence(runes, pos);
77 } else {
78 pos += _handleRegularSequence(runes, pos);
79 }
80 }
81 } catch(e, trace) {
82 stderr.writeln('\nUnexpected exception: $e');
83 stderr.writeln(trace);
84 stderr.close().then((_) {
85 done();
86 });
87 }
88 }
89
90 int _handleControlSequence(List<int> runes, int pos) {
91 var runesConsumed = 1; // Most common result.
92 var char = runes[pos];
93 switch (char) {
94 case runeCtrlA:
95 _home();
96 break;
97
98 case runeCtrlB:
99 _leftArrow();
100 break;
101
102 case runeCtrlD:
103 if (_currentLine.length == 0) {
104 // ^D on an empty line means quit.
105 _stdout.writeln();
106 done();
107 } else {
108 _delete();
109 }
110 break;
111
112 case runeCtrlE:
113 _end();
114 break;
115
116 case runeCtrlF:
117 _rightArrow();
118 break;
119
120 case runeTAB:
121 if (_complete(_tabCount > 1)) {
122 _tabCount = 0;
123 }
124 break;
125
126 case runeNewline:
127 _newline();
128 break;
129
130 case runeCtrlK:
131 _kill();
132 break;
133
134 case runeCtrlL:
135 _clearScreen();
136 break;
137
138 case runeCtrlN:
139 _historyNext();
140 break;
141
142 case runeCtrlP:
143 _historyPrevious();
144 break;
145
146 case runeCtrlU:
147 _clearLine();
148 break;
149
150 case runeCtrlY:
151 _yank();
152 break;
153
154 case runeESC:
155 // Check to see if this is an arrow key.
156 if (pos + 2 < runes.length && // must be a 3 char sequence.
157 runes[pos + 1] == 0x5b) { // second char must be '['.
158 switch (runes[pos + 2]) {
159 case 0x41: // ^[[A = up arrow
160 _historyPrevious();
161 runesConsumed = 3;
162 break;
163
164 case 0x42: // ^[[B = down arrow
165 _historyNext();
166 runesConsumed = 3;
167 break;
168
169 case 0x43: // ^[[C = right arrow
170 _rightArrow();
171 runesConsumed = 3;
172 break;
173
174 case 0x44: // ^[[D = left arrow
175 _leftArrow();
176 runesConsumed = 3;
177 break;
178
179 default:
180 // Ignore the escape character.
181 break;
182 }
183 }
184 break;
185
186 case runeDEL:
187 _backspace();
188 break;
189
190 default:
191 // Ignore the escape character.
192 break;
193 }
194 return runesConsumed;
195 }
196
197 int _handleRegularSequence(List<int> runes, int pos) {
198 var len = pos + 1;
199 while (len < runes.length && !_isControlRune(runes[len])) {
200 len++;
201 }
202 _addChars(runes.getRange(pos, len));
203 return len;
204 }
205
206 bool _isControlRune(int char) {
207 return (char >= 0x00 && char < 0x20) || (char == 0x7f);
208 }
209
210 void done() {
211 _stdin.echoMode = true;
212 _stdin.lineMode = true;
213 _stdinSubscription.cancel();
214 }
215
216 void _writePromptAndLine() {
217 _writePrompt();
218 var pos = _writeRange(_currentLine, 0, _currentLine.length);
219 _cursorPos = _move(pos, _cursorPos);
220 }
221
222 void _writePrompt() {
223 _stdout.write(prompt);
224 }
225
226 void _addChars(Iterable<int> chars) {
227 var newLine = [];
228 newLine..addAll(_currentLine.take(_cursorPos))
229 ..addAll(chars)
230 ..addAll(_currentLine.skip(_cursorPos));
231 _update(newLine, (_cursorPos + chars.length));
232 }
233
234 void _backspace() {
235 if (_cursorPos == 0) {
236 return;
237 }
238
239 var newLine = [];
240 newLine..addAll(_currentLine.take(_cursorPos - 1))
241 ..addAll(_currentLine.skip(_cursorPos));
242 _update(newLine, (_cursorPos - 1));
243 }
244
245 void _delete() {
246 if (_cursorPos == _currentLine.length) {
247 return;
248 }
249
250 var newLine = [];
251 newLine..addAll(_currentLine.take(_cursorPos))
252 ..addAll(_currentLine.skip(_cursorPos + 1));
253 _update(newLine, _cursorPos);
254 }
255
256 void _home() {
257 _updatePos(0);
258 }
259
260 void _end() {
261 _updatePos(_currentLine.length);
262 }
263
264 void _clearScreen() {
265 _stdout.write(_term.clear);
266 _writePromptAndLine();
267 }
268
269 void _kill() {
270 var newLine = [];
271 newLine.addAll(_currentLine.take(_cursorPos));
272 _killBuffer = _currentLine.skip(_cursorPos).toList();
273 _update(newLine, _cursorPos);
274 }
275
276 void _clearLine() {
277 _update([], 0);
278 }
279
280 void _yank() {
281 var newLine = [];
282 newLine..addAll(_currentLine.take(_cursorPos))
283 ..addAll(_killBuffer)
284 ..addAll(_currentLine.skip(_cursorPos));
285 _update(newLine, (_cursorPos + _killBuffer.length));
286 }
287
288 static String _trimLeadingSpaces(String line) {
289 bool _isSpace(int rune) {
290 return rune == runeSpace;
291 }
292 return new String.fromCharCodes(line.runes.skipWhile(_isSpace));
293 }
294
295 static String _sharedPrefix(String one, String two) {
296 var len = min(one.length, two.length);
297 var runesOne = one.runes.toList();
298 var runesTwo = two.runes.toList();
299 var pos;
300 for (pos = 0; pos < len; pos++) {
301 if (runesOne[pos] != runesTwo[pos]) {
302 break;
303 }
304 }
305 var shared = new String.fromCharCodes(runesOne.take(pos));
306 return shared;
307 }
308
309 bool _complete(bool showCompletions) {
310 if (completer == null) {
311 return false;
312 }
313
314 var linePrefix = _currentLine.take(_cursorPos).toList();
315 List<String> commandParts =
316 _trimLeadingSpaces(new String.fromCharCodes(linePrefix)).split(' ');
317 List<String> completionList = completer(commandParts);
318 var completion = '';
319
320 if (completionList.length == 0) {
321 // The current line admits no possible completion.
322 return false;
323
324 } else if (completionList.length == 1) {
325 // There is a single, non-ambiguous completion for the current line.
326 completion = completionList[0];
327
328 // If we are at the end of the line, add a space to signal that
329 // the completion is unambiguous.
330 if (_currentLine.length == _cursorPos) {
331 completion = completion + ' ';
332 }
333 } else {
334 // There are ambiguous completions. Find the longest common
335 // shared prefix of all of the completions.
336 completion = completionList.fold(completionList[0], _sharedPrefix);
337 }
338
339 var lastWord = commandParts.last;
340 if (completion == lastWord) {
341 // The completion does not add anything.
342 if (showCompletions) {
343 // User hit double-TAB. Show them all possible completions.
344 _move(_cursorPos, _currentLine.length);
345 _stdout.writeln();
346 _stdout.writeln(completionList);
347 _writePromptAndLine();
348 }
349 return false;
350 } else {
351 // Apply the current completion.
352 var completionRunes = completion.runes.toList();
353
354 var newLine = [];
355 newLine..addAll(linePrefix)
356 ..addAll(completionRunes.skip(lastWord.length))
357 ..addAll(_currentLine.skip(_cursorPos));
358 _update(newLine, _cursorPos + completionRunes.length - lastWord.length);
359 return true;
360 }
361 }
362
363 void _newline() {
364 _addLineToHistory(_currentLine);
365 _linePos = _lines.length;
366
367 _end();
368 _stdout.writeln();
369
370 // Call the user's command handler.
371 _handleCommand(new String.fromCharCodes(_currentLine));
372
373 _currentLine = [];
374 _cursorPos = 0;
375 _linePos = _lines.length;
376 if (_promptShown) {
377 _writePrompt();
378 }
379 }
380
381 void _leftArrow() {
382 _updatePos(_cursorPos - 1);
383 }
384
385 void _rightArrow() {
386 _updatePos(_cursorPos + 1);
387 }
388
389 void _addLineToHistory(List<int> line) {
390 if (_tempLineAdded) {
391 _lines.removeLast();
392 _tempLineAdded = false;
393 }
394 if (line.length > 0) {
395 _lines.add(line);
396 }
397 }
398
399 void _addTempLineToHistory(List<int> line) {
400 _lines.add(line);
401 _tempLineAdded = true;
402 }
403
404 void _replaceHistory(List<int> line, int linePos) {
405 _lines[linePos] = line;
406 }
407
408 void _historyPrevious() {
409 if (_linePos == 0) {
410 return;
411 }
412
413 if (_linePos == _lines.length) {
414 // The current in-progress line gets temporarily stored in history.
415 _addTempLineToHistory(_currentLine);
416 } else {
417 // Any edits get committed to history.
418 _replaceHistory(_currentLine, _linePos);
419 }
420
421 _linePos -= 1;
422 var line = _lines[_linePos];
423 _update(line, line.length);
424 }
425
426 void _historyNext() {
427 if (_linePos == (_lines.length - 1)) {
428 return;
429 }
430
431 // Any edits get committed to history.
432 _replaceHistory(_currentLine, _linePos);
433
434 _linePos += 1;
435 var line = _lines[_linePos];
436 _update(line, line.length);
437 }
438
439 void _updatePos(int newCursorPos) {
440 if (newCursorPos < 0) {
441 return;
442 }
443 if (newCursorPos > _currentLine.length) {
444 return;
445 }
446
447 _cursorPos = _move(_cursorPos, newCursorPos);
448 }
449
450 void _update(List<int> newLine, int newCursorPos) {
451 var pos = _cursorPos;
452 var diffPos;
453 var sharedLen = min(_currentLine.length, newLine.length);
454
455 // Find first difference.
456 for (diffPos = 0; diffPos < sharedLen; diffPos++) {
457 if (_currentLine[diffPos] != newLine[diffPos]) {
458 break;
459 }
460 }
461
462 // Move the cursor to where the difference begins.
463 pos = _move(pos, diffPos);
464
465 // Write the new text.
466 pos = _writeRange(newLine, pos, newLine.length);
467
468 // Clear any extra characters at the end.
469 pos = _clearRange(pos, _currentLine.length);
470
471 // Move the cursor back to the input point.
472 _cursorPos = _move(pos, newCursorPos);
473 _currentLine = newLine;
474 }
475
476 void hide() {
477 if (!_promptShown) {
478 return;
479 }
480 _promptShown = false;
481 // We need to erase everything, including the prompt.
482 var curLine = _getLine(_cursorPos);
483 var lastLine = _getLine(_currentLine.length);
484
485 // Go to last line.
486 if (curLine < lastLine) {
487 for (var i = 0; i < (lastLine - curLine); i++) {
488 // This moves us to column 0.
489 _stdout.write(_term.cursorDown);
490 }
491 curLine = lastLine;
492 } else {
493 // Move to column 0.
494 _stdout.write('\r');
495 }
496
497 // Work our way up, clearing lines.
498 while (true) {
499 _stdout.write(_term.clrEOL);
500 if (curLine > 0) {
501 _stdout.write(_term.cursorUp);
502 } else {
503 break;
504 }
505 }
506 }
507
508 void show() {
509 if (_promptShown) {
510 return;
511 }
512 _promptShown = true;
513 _writePromptAndLine();
514
515 // If input was buffered while the prompt was hidden, process it
516 // now.
517 if (!_bufferedInput.isEmpty) {
518 var input = _bufferedInput.toString();
519 _bufferedInput.clear();
520 _handleText(input);
521 }
522 }
523
524 int _writeRange(List<int> text, int pos, int writeToPos) {
525 if (pos >= writeToPos) {
526 return pos;
527 }
528 while (pos < writeToPos) {
529 var margin = _nextMargin(pos);
530 var limit = min(writeToPos, margin);
531 _stdout.write(new String.fromCharCodes(text.getRange(pos, limit)));
532 pos = limit;
533 if (pos == margin) {
534 _stdout.write('\n');
535 }
536 }
537 return pos;
538 }
539
540 int _clearRange(int pos, int clearToPos) {
541 if (pos >= clearToPos) {
542 return pos;
543 }
544 while (true) {
545 var limit = _nextMargin(pos);
546 _stdout.write(_term.clrEOL);
547 if (limit >= clearToPos) {
548 return pos;
549 }
550 _stdout.write('\n');
551 pos = limit;
552 }
553 }
554
555 int _move(int pos, int newPos) {
556 if (pos == newPos) {
557 return pos;
558 }
559
560 var curCol = _getCol(pos);
561 var curLine = _getLine(pos);
562 var newCol = _getCol(newPos);
563 var newLine = _getLine(newPos);
564
565 if (curLine > newLine) {
566 for (var i = 0; i < (curLine - newLine); i++) {
567 _stdout.write(_term.cursorUp);
568 }
569 }
570 if (curLine < newLine) {
571 for (var i = 0; i < (newLine - curLine); i++) {
572 _stdout.write(_term.cursorDown);
573 }
574
575 // Moving down resets column to zero, oddly.
576 curCol = 0;
577 }
578 if (curCol > newCol) {
579 for (var i = 0; i < (curCol - newCol); i++) {
580 _stdout.write(_term.cursorBack);
581 }
582 }
583 if (curCol < newCol) {
584 for (var i = 0; i < (newCol - curCol); i++) {
585 _stdout.write(_term.cursorForward);
586 }
587 }
588
589 return newPos;
590 }
591
592 int _nextMargin(int pos) {
593 var truePos = pos + prompt.length;
594 var curLine = _getLine(pos);
595 return ((truePos ~/ _screenWidth) + 1) * _screenWidth - prompt.length;
596 }
597
598 int _getLine(int pos) {
599 var truePos = pos + prompt.length;
600 return truePos ~/ _screenWidth;
601 }
602
603 int _getCol(int pos) {
604 var truePos = pos + prompt.length;
605 return truePos % _screenWidth;
606 }
607
608 Stdin _stdin;
609 StreamSubscription _stdinSubscription;
610 IOSink _stdout;
611 final _handleCommand;
612 final String prompt;
613 bool _promptShown = true;
614 final CommandCompleter completer;
615 TermInfo _term = new TermInfo();
616
617 // TODO(turnidge): Update screenwidth when we clear the screen. See
618 // if we can get screen resize events too.
619 int _screenWidth;
620 List<int> _currentLine = []; // A list of runes.
621 StringBuffer _bufferedInput = new StringBuffer();
622 List<List<int>> _lines = [];
623
624 // When using the command history, the current line is temporarily
625 // added to the history to allow the user to return to it. This
626 // values tracks whether the history has a temporary line at the end.
627 bool _tempLineAdded = false;
628 int _linePos = 0;
629 int _cursorPos = 0;
630 int _tabCount = 0;
631 List<int> _killBuffer = [];
632 }
633
634
635 // Demo code.
636
637
638 List<String> _myCompleter(List<String> commandTokens) {
639 List<String> completions = new List<String>();
640
641 // First word completions.
642 if (commandTokens.length <= 1) {
643 String prefix = '';
644 if (commandTokens.length == 1) {
645 prefix = commandTokens.first;
646 }
647 if ('quit'.startsWith(prefix)) {
648 completions.add('quit');
649 }
650 if ('help'.startsWith(prefix)) {
651 completions.add('help');
652 }
653 if ('happyface'.startsWith(prefix)) {
654 completions.add('happyface');
655 }
656 }
657
658 // Complete 'foobar' or 'gondola' anywhere in string.
659 String lastWord = commandTokens.last;
660 if ('foobar'.startsWith(lastWord)) {
661 completions.add('foobar');
662 }
663 if ('gondola'.startsWith(lastWord)) {
664 completions.add('gondola');
665 }
666
667 return completions;
668 }
669
670
671 int _helpCount = 0;
672 Commando cmdo;
673
674
675 void _handleCommand(String rawCommand) {
676 String command = rawCommand.trim();
677 if (command == 'quit') {
678 cmdo.done();
679 } else if (command == 'help') {
680 switch (_helpCount) {
681 case 0:
682 print('I will not help you.');
683 break;
684 case 1:
685 print('I mean it.');
686 break;
687 case 2:
688 print('Seriously.');
689 break;
690 case 100:
691 print('Well now.');
692 break;
693 default:
694 print("Okay. Type 'quit' to quit");
695 break;
696 }
697 _helpCount++;
698 } else if (command == 'happyface') {
699 print(':-)');
700 } else {
701 print('Received command($command)');
702 }
703 }
704
705
706 void main() {
707 stdout.writeln('[Commando demo]');
708 cmd = new Commando(stdin, stdout, _handleCommand,
709 completer:_myCompleter);
710 }
OLDNEW
« no previous file with comments | « tools/ddbg.dart ('k') | tools/ddbg/lib/terminfo.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698