mirror of
https://github.com/dart-lang/sdk
synced 2024-11-05 18:22:09 +00:00
Allow tab-completion of commands, expression types and named sets
This CL extends the heapsnapshot analysis CLI with tab-completion support for commands, options, filenames, expression types and named sets. This makes it much more comfortable to use the tool. TEST=runtime/tools/heapsnapshot/test/completion_test Change-Id: Iea48b4bd12651a60add6206a92ce06823cbd754a Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/262243 Reviewed-by: Tess Strickland <sstrickl@google.com> Commit-Queue: Martin Kustermann <kustermann@google.com>
This commit is contained in:
parent
f7808a1aeb
commit
aa4339a9f3
7 changed files with 609 additions and 16 deletions
|
@ -4,6 +4,7 @@
|
|||
|
||||
import '../../../tools/heapsnapshot/test/cli_test.dart' as cli_test;
|
||||
import '../../../tools/heapsnapshot/test/expression_test.dart' as expr_test;
|
||||
import '../../../tools/heapsnapshot/test/completion_test.dart' as comp_test;
|
||||
|
||||
import 'use_flag_test_helper.dart';
|
||||
|
||||
|
@ -17,6 +18,7 @@ main(List<String> args) {
|
|||
return;
|
||||
}
|
||||
|
||||
cli_test.main(args);
|
||||
expr_test.main(args);
|
||||
cli_test.main();
|
||||
expr_test.main();
|
||||
comp_test.main();
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:dart_console/dart_console.dart';
|
||||
import 'package:heapsnapshot/src/cli.dart';
|
||||
import 'package:heapsnapshot/src/console.dart';
|
||||
|
||||
class ConsoleErrorPrinter extends Output {
|
||||
final Console console;
|
||||
|
@ -19,8 +19,39 @@ class ConsoleErrorPrinter extends Output {
|
|||
}
|
||||
}
|
||||
|
||||
class CompletionKeyHandler extends KeyHandler {
|
||||
final CliState cliState;
|
||||
CompletionKeyHandler(this.cliState);
|
||||
|
||||
bool handleKey(LineEditBuffer buffer, Key lastPressed) {
|
||||
if (lastPressed.isControl &&
|
||||
lastPressed.controlChar == ControlCharacter.tab &&
|
||||
buffer.completionText.isNotEmpty) {
|
||||
buffer.insert(buffer.completionText);
|
||||
buffer.completionText = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
buffer.completionText = '';
|
||||
|
||||
if (!lastPressed.isControl) {
|
||||
final incomplete = buffer.text.substring(0, buffer.index);
|
||||
final complete = cliCommandRunner.completeCommand(cliState, incomplete);
|
||||
if (complete != null) {
|
||||
if (!complete.startsWith(incomplete)) {
|
||||
throw 'CompletionError: Suggestion "$complete" does not start with "$incomplete".';
|
||||
}
|
||||
if (complete.length > incomplete.length) {
|
||||
buffer.completionText = complete.substring(incomplete.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
final console = Console.scrolling();
|
||||
final console = SmartConsole();
|
||||
|
||||
console.write('The ');
|
||||
console.setForegroundColor(ConsoleColor.brightYellow);
|
||||
|
@ -34,21 +65,18 @@ void main() async {
|
|||
final errors = ConsoleErrorPrinter(console);
|
||||
final cliState = CliState(errors);
|
||||
|
||||
console.completionHandler = CompletionKeyHandler(cliState);
|
||||
|
||||
while (true) {
|
||||
void writePrompt() {
|
||||
console.setForegroundColor(ConsoleColor.brightBlue);
|
||||
console.write('(hsa) ');
|
||||
console.resetColorAttributes();
|
||||
console.setForegroundColor(ConsoleColor.brightGreen);
|
||||
final response = console.smartReadLine();
|
||||
console.resetColorAttributes();
|
||||
if (response.shouldExit) return;
|
||||
if (response.wasCancelled) {
|
||||
console.write(console.newLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
writePrompt();
|
||||
final response = console.readLine(cancelOnEOF: true);
|
||||
console.resetColorAttributes();
|
||||
if (response == null) return;
|
||||
if (response.isEmpty) continue;
|
||||
|
||||
final args = response
|
||||
final args = response.text
|
||||
.split(' ')
|
||||
.map((p) => p.trim())
|
||||
.where((p) => !p.isEmpty)
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:args/args.dart';
|
|||
import 'package:vm_service/vm_service.dart';
|
||||
|
||||
import 'analysis.dart';
|
||||
import 'completion.dart';
|
||||
import 'expression.dart';
|
||||
import 'format.dart';
|
||||
import 'load.dart';
|
||||
|
@ -51,6 +52,39 @@ abstract class Command {
|
|||
}
|
||||
state.output.print(' $usage');
|
||||
}
|
||||
|
||||
String? completeCommand(CliState state, String text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _completeExpression(CliState state, text) {
|
||||
if (!state.isInitialized) return null;
|
||||
|
||||
final output = CompletionCollector();
|
||||
parseExpression(text, output, state.namedSets.names.toSet());
|
||||
return output.suggestedCompletion;
|
||||
}
|
||||
|
||||
String? _completeOptions(String text) {
|
||||
final pc = PostfixCompleter(text);
|
||||
|
||||
final lastWord = getLastWord(text);
|
||||
if (lastWord.isEmpty || !lastWord.startsWith('-')) return null;
|
||||
|
||||
if (!lastWord.startsWith('--')) {
|
||||
// For only one `-` we prefer to complete with abbreviated options.
|
||||
final options = argParser.options.values
|
||||
.where((o) => o.abbr != null)
|
||||
.map((o) => '-' + o.abbr!)
|
||||
.toList();
|
||||
return pc.tryComplete(lastWord, options);
|
||||
}
|
||||
final options = argParser.options.values
|
||||
.expand((o) => [o.name, ...o.aliases])
|
||||
.map((o) => '--$o')
|
||||
.toList();
|
||||
return pc.tryComplete(lastWord, options);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SnapshotCommand extends Command {
|
||||
|
@ -113,6 +147,11 @@ class LoadCommand extends Command {
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String? completeCommand(CliState state, String text) {
|
||||
return tryCompleteFileSystemEntity(
|
||||
text, (filename) => filename.endsWith('.heapsnapshot'));
|
||||
}
|
||||
}
|
||||
|
||||
class StatsCommand extends SnapshotCommand {
|
||||
|
@ -145,6 +184,10 @@ class StatsCommand extends SnapshotCommand {
|
|||
state.analysis.generateObjectStats(oids, sortBySize: !sortByCount);
|
||||
state.output.print(formatHeapStats(stats, maxLines: lines));
|
||||
}
|
||||
|
||||
String? completeCommand(CliState state, String text) {
|
||||
return _completeOptions(text) ?? _completeExpression(state, text);
|
||||
}
|
||||
}
|
||||
|
||||
class DataStatsCommand extends SnapshotCommand {
|
||||
|
@ -176,6 +219,10 @@ class DataStatsCommand extends SnapshotCommand {
|
|||
state.analysis.generateDataStats(oids, sortBySize: !sortByCount);
|
||||
state.output.print(formatDataStats(stats, maxLines: lines));
|
||||
}
|
||||
|
||||
String? completeCommand(CliState state, String text) {
|
||||
return _completeOptions(text) ?? _completeExpression(state, text);
|
||||
}
|
||||
}
|
||||
|
||||
class InfoCommand extends SnapshotCommand {
|
||||
|
@ -255,6 +302,10 @@ class RetainingPathCommand extends SnapshotCommand {
|
|||
state.output.print('');
|
||||
}
|
||||
}
|
||||
|
||||
String? completeCommand(CliState state, String text) {
|
||||
return _completeOptions(text) ?? _completeExpression(state, text);
|
||||
}
|
||||
}
|
||||
|
||||
class ExamineCommand extends SnapshotCommand {
|
||||
|
@ -294,6 +345,10 @@ class ExamineCommand extends SnapshotCommand {
|
|||
if (++i >= limit) break;
|
||||
}
|
||||
}
|
||||
|
||||
String? completeCommand(CliState state, String text) {
|
||||
return _completeOptions(text) ?? _completeExpression(state, text);
|
||||
}
|
||||
}
|
||||
|
||||
class EvaluateCommand extends SnapshotCommand {
|
||||
|
@ -320,6 +375,10 @@ class EvaluateCommand extends SnapshotCommand {
|
|||
}
|
||||
state.output.print(' $name {#${oids.length}}');
|
||||
}
|
||||
|
||||
String? completeCommand(CliState state, String text) {
|
||||
return _completeExpression(state, text);
|
||||
}
|
||||
}
|
||||
|
||||
class DescFilterCommand extends SnapshotCommand {
|
||||
|
@ -383,6 +442,31 @@ class CommandRunner {
|
|||
|
||||
await defaultCommand.execute(state, args);
|
||||
}
|
||||
|
||||
String? completeCommand(CliState state, String text) {
|
||||
// We only complete commands, no arguments (yet).
|
||||
if (text.isEmpty) return null;
|
||||
|
||||
final left = getFirstWordWithSpaces(text);
|
||||
if (left.endsWith(' ')) {
|
||||
final command = name2command[left.trim()];
|
||||
if (command != null) {
|
||||
final right = text.substring(left.length);
|
||||
final result = command.completeCommand(state, right);
|
||||
return (result != null) ? (left + result) : null;
|
||||
}
|
||||
} else {
|
||||
final pc = PostfixCompleter(text);
|
||||
final possibleCommands = name2command.keys
|
||||
.where((name) =>
|
||||
state.isInitialized || name2command[name] is! SnapshotCommand)
|
||||
.toList();
|
||||
final completion = pc.tryComplete(text, possibleCommands);
|
||||
if (completion != null) return completion;
|
||||
}
|
||||
|
||||
return defaultCommand.completeCommand(state, text);
|
||||
}
|
||||
}
|
||||
|
||||
class CliState {
|
||||
|
@ -416,3 +500,11 @@ final cliCommandRunner = CommandRunner([
|
|||
ExamineCommand(),
|
||||
DescFilterCommand(),
|
||||
], EvaluateCommand());
|
||||
|
||||
class CompletionCollector extends Output {
|
||||
String? suggestedCompletion;
|
||||
|
||||
void suggestCompletion(String text) {
|
||||
suggestedCompletion = text;
|
||||
}
|
||||
}
|
||||
|
|
75
runtime/tools/heapsnapshot/lib/src/completion.dart
Normal file
75
runtime/tools/heapsnapshot/lib/src/completion.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class PostfixCompleter {
|
||||
final String text;
|
||||
|
||||
PostfixCompleter(this.text);
|
||||
|
||||
String? tryComplete(String partial, List<String> candidates) {
|
||||
if (!text.endsWith(partial)) throw 'caller error';
|
||||
final completion = _selectCandidate(partial, candidates);
|
||||
if (completion != null) {
|
||||
return text.substring(0, text.length - partial.length) + completion;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? _selectCandidate(String prefix, List<String> candidates) {
|
||||
candidates = candidates
|
||||
.where((c) => prefix.length < c.length && c.startsWith(prefix))
|
||||
.toList();
|
||||
if (candidates.isEmpty) return null;
|
||||
candidates.sort((a, b) => b.length - a.length);
|
||||
return candidates.first;
|
||||
}
|
||||
|
||||
final homePath = Platform.environment['HOME']!;
|
||||
|
||||
String? tryCompleteFileSystemEntity(
|
||||
String incompleteFilePattern, bool Function(String) consider) {
|
||||
if (incompleteFilePattern.isEmpty) return null;
|
||||
|
||||
final filename = incompleteFilePattern.endsWith(path.separator)
|
||||
? ''
|
||||
: path.basename(incompleteFilePattern);
|
||||
final dirname = incompleteFilePattern.substring(
|
||||
0, incompleteFilePattern.length - filename.length);
|
||||
|
||||
final dir = dirname != ''
|
||||
? Directory(dirname.startsWith('~/')
|
||||
? (homePath + dirname.substring(1))
|
||||
: dirname)
|
||||
: Directory.current;
|
||||
|
||||
if (dir.existsSync()) {
|
||||
final entries = dir
|
||||
.listSync()
|
||||
.where((fse) => fse is File && consider(fse.path) || fse is Directory)
|
||||
.map((fse) => path.basename(fse.path))
|
||||
.toList();
|
||||
final pc = PostfixCompleter(incompleteFilePattern);
|
||||
return pc.tryComplete(filename, entries);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
final _spaceCodeUnit = ' '.codeUnitAt(0);
|
||||
|
||||
String getFirstWordWithSpaces(String text) {
|
||||
int i = 0;
|
||||
while (i < text.length && text.codeUnitAt(i) != _spaceCodeUnit) i++;
|
||||
while (i < text.length && text.codeUnitAt(i) == _spaceCodeUnit) i++;
|
||||
return text.substring(0, i);
|
||||
}
|
||||
|
||||
String getLastWord(String text) {
|
||||
return text.substring(text.lastIndexOf(' ') + 1);
|
||||
}
|
292
runtime/tools/heapsnapshot/lib/src/console.dart
Normal file
292
runtime/tools/heapsnapshot/lib/src/console.dart
Normal file
|
@ -0,0 +1,292 @@
|
|||
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:math' show min;
|
||||
|
||||
export 'package:dart_console/dart_console.dart';
|
||||
import 'package:dart_console/dart_console.dart';
|
||||
|
||||
class SmartConsole extends Console {
|
||||
final ScrollbackBuffer history;
|
||||
late final List<KeyHandler> handlers;
|
||||
KeyHandler? completionHandler;
|
||||
|
||||
SmartConsole({this.completionHandler})
|
||||
: history = ScrollbackBuffer(recordBlanks: false) {
|
||||
handlers = [
|
||||
BashNavigationKeyHandler(),
|
||||
BashHistoryKeyHandler(history),
|
||||
BashEditKeyHandler(),
|
||||
];
|
||||
}
|
||||
|
||||
ReadLineResult smartReadLine() {
|
||||
final buffer = LineEditBuffer();
|
||||
|
||||
int promptRow = cursorPosition!.row;
|
||||
|
||||
cursorPosition = Coordinate(promptRow, 0);
|
||||
drawPrompt(buffer);
|
||||
|
||||
while (true) {
|
||||
final key = readKey();
|
||||
|
||||
bool wasHandled = false;
|
||||
for (final handler in handlers) {
|
||||
if (handler.handleKey(buffer, key)) {
|
||||
wasHandled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (completionHandler != null &&
|
||||
completionHandler!.handleKey(buffer, key)) {
|
||||
wasHandled = true;
|
||||
}
|
||||
|
||||
if (!wasHandled && key.isControl) {
|
||||
switch (key.controlChar) {
|
||||
// Accept
|
||||
case ControlCharacter.enter:
|
||||
history.add(buffer.text);
|
||||
writeLine();
|
||||
return ReadLineResult(buffer.text, false, false);
|
||||
|
||||
// Cancel this line.
|
||||
case ControlCharacter.ctrlC:
|
||||
return ReadLineResult('', true, false);
|
||||
|
||||
// EOF
|
||||
case ControlCharacter.ctrlD:
|
||||
if (!buffer.text.isEmpty) break;
|
||||
return ReadLineResult('', true, true);
|
||||
|
||||
// Ignore.
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cursorPosition = Coordinate(promptRow, 0);
|
||||
drawPrompt(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void drawPrompt(LineEditBuffer buffer) {
|
||||
const prefix = '(hsa) ';
|
||||
|
||||
final row = cursorPosition!.row;
|
||||
|
||||
setForegroundColor(ConsoleColor.brightBlue);
|
||||
write(prefix);
|
||||
resetColorAttributes();
|
||||
setForegroundColor(ConsoleColor.brightGreen);
|
||||
|
||||
eraseCursorToEnd();
|
||||
if (buffer.completionText.isNotEmpty) {
|
||||
write(buffer.text.substring(0, buffer.index));
|
||||
setForegroundColor(ConsoleColor.brightWhite);
|
||||
write(buffer.completionText);
|
||||
setForegroundColor(ConsoleColor.brightGreen);
|
||||
write(buffer.text.substring(buffer.index));
|
||||
} else {
|
||||
write(buffer.text);
|
||||
}
|
||||
|
||||
cursorPosition = Coordinate(row, prefix.length + buffer.index);
|
||||
}
|
||||
}
|
||||
|
||||
class ReadLineResult {
|
||||
final String text;
|
||||
final bool wasCancelled;
|
||||
final bool shouldExit;
|
||||
|
||||
ReadLineResult(this.text, this.wasCancelled, this.shouldExit);
|
||||
}
|
||||
|
||||
/// Handler of a new key stroke when editing a line.
|
||||
abstract class KeyHandler {
|
||||
bool handleKey(LineEditBuffer buffer, Key key);
|
||||
}
|
||||
|
||||
/// Handles cursor navigation.
|
||||
class BashNavigationKeyHandler extends KeyHandler {
|
||||
bool handleKey(LineEditBuffer buffer, Key key) {
|
||||
if (!key.isControl) return false;
|
||||
|
||||
switch (key.controlChar) {
|
||||
case ControlCharacter.arrowLeft:
|
||||
case ControlCharacter.ctrlB:
|
||||
buffer.moveLeft();
|
||||
return true;
|
||||
case ControlCharacter.arrowRight:
|
||||
case ControlCharacter.ctrlF:
|
||||
buffer.moveRight();
|
||||
return true;
|
||||
case ControlCharacter.wordLeft:
|
||||
buffer.moveWordLeft();
|
||||
return true;
|
||||
case ControlCharacter.wordRight:
|
||||
buffer.moveWordRight();
|
||||
return true;
|
||||
case ControlCharacter.home:
|
||||
case ControlCharacter.ctrlA:
|
||||
buffer.moveStart();
|
||||
return true;
|
||||
case ControlCharacter.end:
|
||||
case ControlCharacter.ctrlE:
|
||||
buffer.moveEnd();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles history navigation.
|
||||
class BashHistoryKeyHandler extends KeyHandler {
|
||||
ScrollbackBuffer history;
|
||||
|
||||
BashHistoryKeyHandler(this.history);
|
||||
|
||||
bool handleKey(LineEditBuffer buffer, Key key) {
|
||||
if (!key.isControl) return false;
|
||||
|
||||
switch (key.controlChar) {
|
||||
case ControlCharacter.ctrlP:
|
||||
case ControlCharacter.arrowUp:
|
||||
buffer.replaceWith(history.up(buffer.text));
|
||||
return true;
|
||||
case ControlCharacter.ctrlN:
|
||||
case ControlCharacter.arrowDown:
|
||||
final temp = history.down();
|
||||
if (temp != null) {
|
||||
buffer.replaceWith(temp);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles text edits.
|
||||
class BashEditKeyHandler extends KeyHandler {
|
||||
bool handleKey(LineEditBuffer buffer, Key key) {
|
||||
if (!key.isControl) {
|
||||
buffer.insert(key.char);
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Add support for <alt-d> (delete word).
|
||||
switch (key.controlChar) {
|
||||
case ControlCharacter.backspace:
|
||||
case ControlCharacter.ctrlH:
|
||||
buffer.backspace();
|
||||
return true;
|
||||
case ControlCharacter.ctrlU:
|
||||
buffer.truncateLeft();
|
||||
return true;
|
||||
case ControlCharacter.ctrlK:
|
||||
buffer.truncateRight();
|
||||
return true;
|
||||
case ControlCharacter.delete:
|
||||
case ControlCharacter.ctrlD:
|
||||
final wasDeleted = buffer.delete();
|
||||
return wasDeleted;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of [Console.readLine] while editing the line.
|
||||
class LineEditBuffer {
|
||||
/// The _text that was so far entered.
|
||||
String _text = '';
|
||||
|
||||
/// The _index into [_text] where the editing cursor is currently being drawn.
|
||||
int _index = 0;
|
||||
|
||||
/// The _text to display as inline completion.
|
||||
String completionText = '';
|
||||
|
||||
LineEditBuffer();
|
||||
|
||||
String get text => _text;
|
||||
|
||||
int get index => _index;
|
||||
|
||||
void moveWordLeft() {
|
||||
if (_index > 0) {
|
||||
final textLeftOfCursor = _text.substring(0, _index - 1);
|
||||
final lastSpace = textLeftOfCursor.lastIndexOf(' ');
|
||||
_index = lastSpace != -1 ? lastSpace + 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
void moveWordRight() {
|
||||
if (_index < _text.length) {
|
||||
final textRightOfCursor = _text.substring(_index + 1);
|
||||
final nextSpace = textRightOfCursor.indexOf(' ');
|
||||
_index = nextSpace != -1
|
||||
? min(_index + nextSpace + 2, _text.length)
|
||||
: _text.length;
|
||||
}
|
||||
}
|
||||
|
||||
void moveLeft() {
|
||||
if (_index > 0) _index--;
|
||||
}
|
||||
|
||||
void moveRight() {
|
||||
if (_index < _text.length) _index++;
|
||||
}
|
||||
|
||||
void moveStart() {
|
||||
_index = 0;
|
||||
}
|
||||
|
||||
void moveEnd() {
|
||||
_index = _text.length;
|
||||
}
|
||||
|
||||
void replaceWith(String newtext) {
|
||||
_text = newtext;
|
||||
_index = _text.length;
|
||||
}
|
||||
|
||||
void truncateRight() {
|
||||
_text = _text.substring(0, _index);
|
||||
}
|
||||
|
||||
void truncateLeft() {
|
||||
_text = _text.substring(_index, _text.length);
|
||||
_index = 0;
|
||||
}
|
||||
|
||||
void backspace() {
|
||||
if (_index > 0) {
|
||||
_text = _text.substring(0, _index - 1) + _text.substring(_index);
|
||||
_index--;
|
||||
}
|
||||
}
|
||||
|
||||
bool delete() {
|
||||
if (_index < _text.length) {
|
||||
_text = _text.substring(0, _index) + _text.substring(_index + 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void insert(String chars) {
|
||||
if (_index == _text.length) {
|
||||
_text += chars;
|
||||
} else {
|
||||
_text = _text.substring(0, _index) + chars + _text.substring(_index);
|
||||
}
|
||||
_index += chars.length;
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import 'dart:math' as math;
|
|||
import 'package:vm_service/vm_service.dart';
|
||||
|
||||
import 'analysis.dart';
|
||||
import 'completion.dart';
|
||||
|
||||
abstract class SetExpression {
|
||||
Set<int>? evaluate(NamedSets namedSets, Analysis analysis, Output output);
|
||||
|
@ -453,6 +454,17 @@ SetExpression? parse(
|
|||
}
|
||||
if (!namedSets.contains(current)) {
|
||||
output.printError('There is no set with name "$current". See `info`.');
|
||||
|
||||
// We're at the end - it may be beneficial to suggest completion.
|
||||
if (tokens.isAtEnd && tokens._text.endsWith(current)) {
|
||||
final pc = PostfixCompleter(tokens._text);
|
||||
final candidate = pc.tryComplete(current, namedSets.toList()) ??
|
||||
pc.tryComplete(current, parsingFunctions.keys.toList());
|
||||
if (candidate != null) {
|
||||
output.suggestCompletion(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return NamedExpression(current);
|
||||
|
@ -632,6 +644,7 @@ class NamedSets {
|
|||
abstract class Output {
|
||||
void print(String message) {}
|
||||
void printError(String message) {}
|
||||
void suggestCompletion(String text) {}
|
||||
}
|
||||
|
||||
const dslDescription = '''
|
||||
|
|
91
runtime/tools/heapsnapshot/test/completion_test.dart
Normal file
91
runtime/tools/heapsnapshot/test/completion_test.dart
Normal file
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:heapsnapshot/heapsnapshot.dart';
|
||||
import 'package:heapsnapshot/src/cli.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class FakeAnalysis implements Analysis {
|
||||
@override
|
||||
Set<int> get roots => <int>{1};
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation i) {}
|
||||
}
|
||||
|
||||
main() {
|
||||
late CliState cliState;
|
||||
|
||||
String? complete(String text) =>
|
||||
cliCommandRunner.completeCommand(cliState, text);
|
||||
|
||||
group('cli-completion no snapshot loaded', () {
|
||||
setUp(() {
|
||||
cliState = CliState(CompletionCollector());
|
||||
});
|
||||
|
||||
// <...incomplete-load-command...>
|
||||
test('complete load command', () {
|
||||
expect(complete('l'), 'load');
|
||||
});
|
||||
|
||||
// <...incomplete-stats-command...> fails
|
||||
test('complete stats command fails', () {
|
||||
// Since there was no snapshot loaded, commands operating on loaded
|
||||
// snapshot should not auto-complete yet.
|
||||
expect(complete('s'), null);
|
||||
});
|
||||
|
||||
// load <...incomplete-file...>
|
||||
test('complete incomplete file', () {
|
||||
final snapshotDir = Directory.systemTemp.createTempSync('snapshot');
|
||||
try {
|
||||
final file = path.join(snapshotDir.path, 'foobar.heapsnapshot');
|
||||
File(file).createSync();
|
||||
|
||||
// Ensure auto-complete works for files.
|
||||
expect(complete('load ${path.join(snapshotDir.path, 'fo')}'),
|
||||
'load $file');
|
||||
} finally {
|
||||
snapshotDir.deleteSync(recursive: true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('cli-completion snapshot loaded', () {
|
||||
setUp(() {
|
||||
cliState = CliState(CompletionCollector());
|
||||
cliState.initialize(FakeAnalysis());
|
||||
});
|
||||
|
||||
// <...incomplete-command...>
|
||||
test('complete command', () {
|
||||
expect(complete('s'), 'stats');
|
||||
});
|
||||
|
||||
// <command> <...incomplete-option...>
|
||||
test('complete command short option', () {
|
||||
expect(complete('stats -'), 'stats -c');
|
||||
});
|
||||
test('complete command long option', () {
|
||||
expect(complete('stats --m'), 'stats --max');
|
||||
});
|
||||
|
||||
// <command> <...incomplete-args...>
|
||||
test('complete command arg', () {
|
||||
cliState.namedSets.nameSet({1}, 'foobar');
|
||||
expect(complete('stats f'), 'stats foobar');
|
||||
});
|
||||
|
||||
// <expr>
|
||||
test('complete default eval command', () {
|
||||
cliState.namedSets.nameSet({1}, 'foobar');
|
||||
expect(complete('foo'), 'foobar');
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue