Tab-completion in REPL based on module variables.

This commit is contained in:
Bob Nystrom
2016-05-22 15:32:17 -07:00
parent 98514188b0
commit f47e128978
8 changed files with 177 additions and 25 deletions

View File

@ -1,13 +1,6 @@
import "meta" for Meta import "meta" for Meta
import "io" for Stdin import "io" for Stdin
class EscapeBracket {
static up { 65 }
static down { 66 }
static right { 67 }
static left { 68 }
}
class Repl { class Repl {
construct new() { construct new() {
_cursor = 0 _cursor = 0
@ -19,7 +12,7 @@ class Repl {
run() { run() {
Stdin.isRaw = true Stdin.isRaw = true
refreshLine() refreshLine(false)
while (true) { while (true) {
var byte = Stdin.readByte() var byte = Stdin.readByte()
@ -32,7 +25,7 @@ class Repl {
return return
} else if (byte == Chars.ctrlD) { } else if (byte == Chars.ctrlD) {
// If the line is empty, Ctrl_D exits. // If the line is empty, Ctrl_D exits.
if (!_line.isEmpty) { if (_line.isEmpty) {
System.print() System.print()
return return
} }
@ -43,6 +36,12 @@ class Repl {
_cursor = _line.count _cursor = _line.count
} else if (byte == Chars.ctrlF) { } else if (byte == Chars.ctrlF) {
cursorRight() cursorRight()
} else if (byte == Chars.tab) {
var completion = getCompletion()
if (completion != null) {
_line = _line + completion
_cursor = _line.count
}
} else if (byte == Chars.ctrlK) { } else if (byte == Chars.ctrlK) {
// Delete everything after the cursor. // Delete everything after the cursor.
_line = _line[0..._cursor] _line = _line[0..._cursor]
@ -77,7 +76,7 @@ class Repl {
System.print("Unhandled byte: %(byte)") System.print("Unhandled byte: %(byte)")
} }
refreshLine() refreshLine(true)
} }
} }
@ -157,6 +156,9 @@ class Repl {
} }
executeInput() { executeInput() {
// Remove the completion hint.
refreshLine(false)
// Add it to the history. // Add it to the history.
_history.add(_line) _history.add(_line)
_historyIndex = _history.count _historyIndex = _history.count
@ -191,12 +193,14 @@ class Repl {
var fiber var fiber
if (isStatement) { if (isStatement) {
fiber = Fiber.new { fiber = Fiber.new {
// TODO: Should evaluate in main module, not repl's own.
Meta.eval(input) Meta.eval(input)
} }
var result = fiber.try() var result = fiber.try()
if (fiber.error == null) return if (fiber.error == null) return
} else { } else {
// TODO: Should evaluate in main module, not repl's own.
var function = Meta.compileExpression(input) var function = Meta.compileExpression(input)
if (function == null) return if (function == null) return
@ -204,6 +208,9 @@ class Repl {
var result = fiber.try() var result = fiber.try()
if (fiber.error == null) { if (fiber.error == null) {
// TODO: Handle error in result.toString. // TODO: Handle error in result.toString.
// TODO: Syntax color based on type? It might be nice to distinguish
// between string results versus stringified results. Otherwise, the
// user can't tell the difference between `true` and "true".
System.print("%(Color.brightWhite)%(result)%(Color.none)") System.print("%(Color.brightWhite)%(result)%(Color.none)")
return return
} }
@ -229,7 +236,7 @@ class Repl {
return tokens return tokens
} }
refreshLine() { refreshLine(showCompletion) {
// Erase the whole line. // Erase the whole line.
System.write("\x1b[2K") System.write("\x1b[2K")
@ -247,9 +254,32 @@ class Repl {
System.write(Color.none) System.write(Color.none)
} }
if (showCompletion) {
var completion = getCompletion()
if (completion != null) {
System.write("%(Color.gray)%(completion)%(Color.none)")
}
}
// Position the cursor. // Position the cursor.
System.write("\r\x1b[%(2 + _cursor)C") System.write("\r\x1b[%(2 + _cursor)C")
} }
/// Gets the best possible auto-completion for the current line, or null if
/// there is none. The completion is the remaining string to append to the
/// line, not the entire completed line.
getCompletion() {
if (_line.isEmpty) return null
// Only complete if the cursor is at the end.
if (_cursor != _line.count) return null
for (name in Meta.getModuleVariables("repl")) {
if (name.startsWith(_line)) {
return name[_line.count..-1]
}
}
}
} }
/// ANSI color escape sequences. /// ANSI color escape sequences.
@ -351,6 +381,13 @@ class Chars {
static isWhitespace(c) { c == space || c == tab || c == carriageReturn } static isWhitespace(c) { c == space || c == tab || c == carriageReturn }
} }
class EscapeBracket {
static up { 0x41 }
static down { 0x42 }
static right { 0x43 }
static left { 0x44 }
}
class Token { class Token {
// Punctuators. // Punctuators.
static leftParen { "leftParen" } static leftParen { "leftParen" }

View File

@ -3,13 +3,6 @@ static const char* replModuleSource =
"import \"meta\" for Meta\n" "import \"meta\" for Meta\n"
"import \"io\" for Stdin\n" "import \"io\" for Stdin\n"
"\n" "\n"
"class EscapeBracket {\n"
" static up { 65 }\n"
" static down { 66 }\n"
" static right { 67 }\n"
" static left { 68 }\n"
"}\n"
"\n"
"class Repl {\n" "class Repl {\n"
" construct new() {\n" " construct new() {\n"
" _cursor = 0\n" " _cursor = 0\n"
@ -21,7 +14,7 @@ static const char* replModuleSource =
"\n" "\n"
" run() {\n" " run() {\n"
" Stdin.isRaw = true\n" " Stdin.isRaw = true\n"
" refreshLine()\n" " refreshLine(false)\n"
"\n" "\n"
" while (true) {\n" " while (true) {\n"
" var byte = Stdin.readByte()\n" " var byte = Stdin.readByte()\n"
@ -34,7 +27,7 @@ static const char* replModuleSource =
" return\n" " return\n"
" } else if (byte == Chars.ctrlD) {\n" " } else if (byte == Chars.ctrlD) {\n"
" // If the line is empty, Ctrl_D exits.\n" " // If the line is empty, Ctrl_D exits.\n"
" if (!_line.isEmpty) {\n" " if (_line.isEmpty) {\n"
" System.print()\n" " System.print()\n"
" return\n" " return\n"
" }\n" " }\n"
@ -45,6 +38,12 @@ static const char* replModuleSource =
" _cursor = _line.count\n" " _cursor = _line.count\n"
" } else if (byte == Chars.ctrlF) {\n" " } else if (byte == Chars.ctrlF) {\n"
" cursorRight()\n" " cursorRight()\n"
" } else if (byte == Chars.tab) {\n"
" var completion = getCompletion()\n"
" if (completion != null) {\n"
" _line = _line + completion\n"
" _cursor = _line.count\n"
" }\n"
" } else if (byte == Chars.ctrlK) {\n" " } else if (byte == Chars.ctrlK) {\n"
" // Delete everything after the cursor.\n" " // Delete everything after the cursor.\n"
" _line = _line[0..._cursor]\n" " _line = _line[0..._cursor]\n"
@ -73,14 +72,13 @@ static const char* replModuleSource =
" // TODO: Ctrl-T to swap chars.\n" " // TODO: Ctrl-T to swap chars.\n"
" // TODO: ESC H and F to move to beginning and end of line. (Both ESC\n" " // TODO: ESC H and F to move to beginning and end of line. (Both ESC\n"
" // [ and ESC 0 sequences?)\n" " // [ and ESC 0 sequences?)\n"
" // TODO: Ctrl-L clear screen.\n"
" // TODO: Ctrl-W delete previous word.\n" " // TODO: Ctrl-W delete previous word.\n"
" // TODO: Completion.\n" " // TODO: Completion.\n"
" // TODO: Other shortcuts?\n" " // TODO: Other shortcuts?\n"
" System.print(\"Unhandled byte: %(byte)\")\n" " System.print(\"Unhandled byte: %(byte)\")\n"
" }\n" " }\n"
"\n" "\n"
" refreshLine()\n" " refreshLine(true)\n"
" }\n" " }\n"
" }\n" " }\n"
"\n" "\n"
@ -160,6 +158,9 @@ static const char* replModuleSource =
" }\n" " }\n"
"\n" "\n"
" executeInput() {\n" " executeInput() {\n"
" // Remove the completion hint.\n"
" refreshLine(false)\n"
"\n"
" // Add it to the history.\n" " // Add it to the history.\n"
" _history.add(_line)\n" " _history.add(_line)\n"
" _historyIndex = _history.count\n" " _historyIndex = _history.count\n"
@ -194,12 +195,14 @@ static const char* replModuleSource =
" var fiber\n" " var fiber\n"
" if (isStatement) {\n" " if (isStatement) {\n"
" fiber = Fiber.new {\n" " fiber = Fiber.new {\n"
" // TODO: Should evaluate in main module, not repl's own.\n"
" Meta.eval(input)\n" " Meta.eval(input)\n"
" }\n" " }\n"
"\n" "\n"
" var result = fiber.try()\n" " var result = fiber.try()\n"
" if (fiber.error == null) return\n" " if (fiber.error == null) return\n"
" } else {\n" " } else {\n"
" // TODO: Should evaluate in main module, not repl's own.\n"
" var function = Meta.compileExpression(input)\n" " var function = Meta.compileExpression(input)\n"
" if (function == null) return\n" " if (function == null) return\n"
"\n" "\n"
@ -207,6 +210,9 @@ static const char* replModuleSource =
" var result = fiber.try()\n" " var result = fiber.try()\n"
" if (fiber.error == null) {\n" " if (fiber.error == null) {\n"
" // TODO: Handle error in result.toString.\n" " // TODO: Handle error in result.toString.\n"
" // TODO: Syntax color based on type? It might be nice to distinguish\n"
" // between string results versus stringified results. Otherwise, the\n"
" // user can't tell the difference between `true` and \"true\".\n"
" System.print(\"%(Color.brightWhite)%(result)%(Color.none)\")\n" " System.print(\"%(Color.brightWhite)%(result)%(Color.none)\")\n"
" return\n" " return\n"
" }\n" " }\n"
@ -232,7 +238,7 @@ static const char* replModuleSource =
" return tokens\n" " return tokens\n"
" }\n" " }\n"
"\n" "\n"
" refreshLine() {\n" " refreshLine(showCompletion) {\n"
" // Erase the whole line.\n" " // Erase the whole line.\n"
" System.write(\"\x1b[2K\")\n" " System.write(\"\x1b[2K\")\n"
"\n" "\n"
@ -250,9 +256,32 @@ static const char* replModuleSource =
" System.write(Color.none)\n" " System.write(Color.none)\n"
" }\n" " }\n"
"\n" "\n"
" if (showCompletion) {\n"
" var completion = getCompletion()\n"
" if (completion != null) {\n"
" System.write(\"%(Color.gray)%(completion)%(Color.none)\")\n"
" }\n"
" }\n"
"\n"
" // Position the cursor.\n" " // Position the cursor.\n"
" System.write(\"\r\x1b[%(2 + _cursor)C\")\n" " System.write(\"\r\x1b[%(2 + _cursor)C\")\n"
" }\n" " }\n"
"\n"
" /// Gets the best possible auto-completion for the current line, or null if\n"
" /// there is none. The completion is the remaining string to append to the\n"
" /// line, not the entire completed line.\n"
" getCompletion() {\n"
" if (_line.isEmpty) return null\n"
"\n"
" // Only complete if the cursor is at the end.\n"
" if (_cursor != _line.count) return null\n"
"\n"
" for (name in Meta.getModuleVariables(\"repl\")) {\n"
" if (name.startsWith(_line)) {\n"
" return name[_line.count..-1]\n"
" }\n"
" }\n"
" }\n"
"}\n" "}\n"
"\n" "\n"
"/// ANSI color escape sequences.\n" "/// ANSI color escape sequences.\n"
@ -354,6 +383,13 @@ static const char* replModuleSource =
" static isWhitespace(c) { c == space || c == tab || c == carriageReturn }\n" " static isWhitespace(c) { c == space || c == tab || c == carriageReturn }\n"
"}\n" "}\n"
"\n" "\n"
"class EscapeBracket {\n"
" static up { 0x41 }\n"
" static down { 0x42 }\n"
" static right { 0x43 }\n"
" static left { 0x44 }\n"
"}\n"
"\n"
"class Token {\n" "class Token {\n"
" // Punctuators.\n" " // Punctuators.\n"
" static leftParen { \"leftParen\" }\n" " static leftParen { \"leftParen\" }\n"

View File

@ -36,6 +36,34 @@ void metaCompile(WrenVM* vm)
} }
} }
void metaGetModuleVariables(WrenVM* vm) {
wrenEnsureSlots(vm, 3);
Value moduleValue = wrenMapGet(vm->modules, vm->apiStack[1]);
if (IS_UNDEFINED(moduleValue))
{
vm->apiStack[0] = NULL_VAL;
return;
}
ObjModule* module = AS_MODULE(moduleValue);
ObjList* names = wrenNewList(vm, module->variableNames.count);
vm->apiStack[0] = OBJ_VAL(names);
// Initialize the elements to null in case a collection happens when we
// allocate the strings below.
for (int i = 0; i < names->elements.count; i++)
{
names->elements.data[i] = NULL_VAL;
}
for (int i = 0; i < names->elements.count; i++)
{
String* name = &module->variableNames.data[i];
names->elements.data[i] = wrenNewString(vm, name->buffer, name->length);
}
}
const char* wrenMetaSource() const char* wrenMetaSource()
{ {
return metaModuleSource; return metaModuleSource;
@ -49,9 +77,19 @@ WrenForeignMethodFn wrenMetaBindForeignMethod(WrenVM* vm,
// There is only one foreign method in the meta module. // There is only one foreign method in the meta module.
ASSERT(strcmp(className, "Meta") == 0, "Should be in Meta class."); ASSERT(strcmp(className, "Meta") == 0, "Should be in Meta class.");
ASSERT(isStatic, "Should be static."); ASSERT(isStatic, "Should be static.");
ASSERT(strcmp(signature, "compile_(_,_,_)") == 0, "Should be compile method.");
return metaCompile; if (strcmp(signature, "compile_(_,_,_)") == 0)
{
return metaCompile;
}
if (strcmp(signature, "getModuleVariables_(_)") == 0)
{
return metaGetModuleVariables;
}
ASSERT(false, "Unknown method.");
return NULL;
} }
#endif #endif

View File

@ -1,4 +1,12 @@
class Meta { class Meta {
static getModuleVariables(module) {
if (!(module is String)) Fiber.abort("Module name must be a string.")
var result = getModuleVariables_(module)
if (result != null) return result
Fiber.abort("Could not find a module named '%(module)'.")
}
static eval(source) { static eval(source) {
if (!(source is String)) Fiber.abort("Source code must be a string.") if (!(source is String)) Fiber.abort("Source code must be a string.")
@ -15,4 +23,5 @@ class Meta {
} }
foreign static compile_(source, isExpression, printErrors) foreign static compile_(source, isExpression, printErrors)
foreign static getModuleVariables_(module)
} }

View File

@ -1,6 +1,14 @@
// Generated automatically from src/optional/wren_opt_meta.wren. Do not edit. // Generated automatically from src/optional/wren_opt_meta.wren. Do not edit.
static const char* metaModuleSource = static const char* metaModuleSource =
"class Meta {\n" "class Meta {\n"
" static getModuleVariables(module) {\n"
" if (!(module is String)) Fiber.abort(\"Module name must be a string.\")\n"
" var result = getModuleVariables_(module)\n"
" if (result != null) return result\n"
"\n"
" Fiber.abort(\"Could not find a module named '%(module)'.\")\n"
" }\n"
"\n"
" static eval(source) {\n" " static eval(source) {\n"
" if (!(source is String)) Fiber.abort(\"Source code must be a string.\")\n" " if (!(source is String)) Fiber.abort(\"Source code must be a string.\")\n"
"\n" "\n"
@ -17,4 +25,5 @@ static const char* metaModuleSource =
" }\n" " }\n"
"\n" "\n"
" foreign static compile_(source, isExpression, printErrors)\n" " foreign static compile_(source, isExpression, printErrors)\n"
" foreign static getModuleVariables_(module)\n"
"}\n"; "}\n";

View File

@ -0,0 +1,17 @@
import "meta" for Meta
var variables = Meta.getModuleVariables("main")
// Includes implicitly imported core stuff.
System.print(variables.contains("Object")) // expect: true
System.print(variables.contains("Bool")) // expect: true
// Includes top level variables.
System.print(variables.contains("variables")) // expect: true
// Even ones declared later.
System.print(variables.contains("later")) // expect: true
var later = "values"
System.print(variables.contains("unknown")) // expect: false

View File

@ -0,0 +1,3 @@
import "meta" for Meta
Meta.getModuleVariables(123) // expect runtime error: Module name must be a string.

View File

@ -0,0 +1,3 @@
import "meta" for Meta
Meta.getModuleVariables("unknown") // expect runtime error: Could not find a module named 'unknown'.