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 "io" for Stdin
class EscapeBracket {
static up { 65 }
static down { 66 }
static right { 67 }
static left { 68 }
}
class Repl {
construct new() {
_cursor = 0
@ -19,7 +12,7 @@ class Repl {
run() {
Stdin.isRaw = true
refreshLine()
refreshLine(false)
while (true) {
var byte = Stdin.readByte()
@ -32,7 +25,7 @@ class Repl {
return
} else if (byte == Chars.ctrlD) {
// If the line is empty, Ctrl_D exits.
if (!_line.isEmpty) {
if (_line.isEmpty) {
System.print()
return
}
@ -43,6 +36,12 @@ class Repl {
_cursor = _line.count
} else if (byte == Chars.ctrlF) {
cursorRight()
} else if (byte == Chars.tab) {
var completion = getCompletion()
if (completion != null) {
_line = _line + completion
_cursor = _line.count
}
} else if (byte == Chars.ctrlK) {
// Delete everything after the cursor.
_line = _line[0..._cursor]
@ -77,7 +76,7 @@ class Repl {
System.print("Unhandled byte: %(byte)")
}
refreshLine()
refreshLine(true)
}
}
@ -157,6 +156,9 @@ class Repl {
}
executeInput() {
// Remove the completion hint.
refreshLine(false)
// Add it to the history.
_history.add(_line)
_historyIndex = _history.count
@ -191,12 +193,14 @@ class Repl {
var fiber
if (isStatement) {
fiber = Fiber.new {
// TODO: Should evaluate in main module, not repl's own.
Meta.eval(input)
}
var result = fiber.try()
if (fiber.error == null) return
} else {
// TODO: Should evaluate in main module, not repl's own.
var function = Meta.compileExpression(input)
if (function == null) return
@ -204,6 +208,9 @@ class Repl {
var result = fiber.try()
if (fiber.error == null) {
// 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)")
return
}
@ -229,7 +236,7 @@ class Repl {
return tokens
}
refreshLine() {
refreshLine(showCompletion) {
// Erase the whole line.
System.write("\x1b[2K")
@ -247,9 +254,32 @@ class Repl {
System.write(Color.none)
}
if (showCompletion) {
var completion = getCompletion()
if (completion != null) {
System.write("%(Color.gray)%(completion)%(Color.none)")
}
}
// Position the cursor.
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.
@ -351,6 +381,13 @@ class Chars {
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 {
// Punctuators.
static leftParen { "leftParen" }

View File

@ -3,13 +3,6 @@ static const char* replModuleSource =
"import \"meta\" for Meta\n"
"import \"io\" for Stdin\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"
" construct new() {\n"
" _cursor = 0\n"
@ -21,7 +14,7 @@ static const char* replModuleSource =
"\n"
" run() {\n"
" Stdin.isRaw = true\n"
" refreshLine()\n"
" refreshLine(false)\n"
"\n"
" while (true) {\n"
" var byte = Stdin.readByte()\n"
@ -34,7 +27,7 @@ static const char* replModuleSource =
" return\n"
" } else if (byte == Chars.ctrlD) {\n"
" // If the line is empty, Ctrl_D exits.\n"
" if (!_line.isEmpty) {\n"
" if (_line.isEmpty) {\n"
" System.print()\n"
" return\n"
" }\n"
@ -45,6 +38,12 @@ static const char* replModuleSource =
" _cursor = _line.count\n"
" } else if (byte == Chars.ctrlF) {\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"
" // Delete everything after the cursor.\n"
" _line = _line[0..._cursor]\n"
@ -73,14 +72,13 @@ static const char* replModuleSource =
" // TODO: Ctrl-T to swap chars.\n"
" // TODO: ESC H and F to move to beginning and end of line. (Both ESC\n"
" // [ and ESC 0 sequences?)\n"
" // TODO: Ctrl-L clear screen.\n"
" // TODO: Ctrl-W delete previous word.\n"
" // TODO: Completion.\n"
" // TODO: Other shortcuts?\n"
" System.print(\"Unhandled byte: %(byte)\")\n"
" }\n"
"\n"
" refreshLine()\n"
" refreshLine(true)\n"
" }\n"
" }\n"
"\n"
@ -160,6 +158,9 @@ static const char* replModuleSource =
" }\n"
"\n"
" executeInput() {\n"
" // Remove the completion hint.\n"
" refreshLine(false)\n"
"\n"
" // Add it to the history.\n"
" _history.add(_line)\n"
" _historyIndex = _history.count\n"
@ -194,12 +195,14 @@ static const char* replModuleSource =
" var fiber\n"
" if (isStatement) {\n"
" fiber = Fiber.new {\n"
" // TODO: Should evaluate in main module, not repl's own.\n"
" Meta.eval(input)\n"
" }\n"
"\n"
" var result = fiber.try()\n"
" if (fiber.error == null) return\n"
" } else {\n"
" // TODO: Should evaluate in main module, not repl's own.\n"
" var function = Meta.compileExpression(input)\n"
" if (function == null) return\n"
"\n"
@ -207,6 +210,9 @@ static const char* replModuleSource =
" var result = fiber.try()\n"
" if (fiber.error == null) {\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"
" return\n"
" }\n"
@ -232,7 +238,7 @@ static const char* replModuleSource =
" return tokens\n"
" }\n"
"\n"
" refreshLine() {\n"
" refreshLine(showCompletion) {\n"
" // Erase the whole line.\n"
" System.write(\"\x1b[2K\")\n"
"\n"
@ -250,9 +256,32 @@ static const char* replModuleSource =
" System.write(Color.none)\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"
" System.write(\"\r\x1b[%(2 + _cursor)C\")\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"
"/// ANSI color escape sequences.\n"
@ -354,6 +383,13 @@ static const char* replModuleSource =
" static isWhitespace(c) { c == space || c == tab || c == carriageReturn }\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"
" // Punctuators.\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()
{
return metaModuleSource;
@ -49,9 +77,19 @@ WrenForeignMethodFn wrenMetaBindForeignMethod(WrenVM* vm,
// There is only one foreign method in the meta module.
ASSERT(strcmp(className, "Meta") == 0, "Should be in Meta class.");
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

View File

@ -1,4 +1,12 @@
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) {
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 getModuleVariables_(module)
}

View File

@ -1,6 +1,14 @@
// Generated automatically from src/optional/wren_opt_meta.wren. Do not edit.
static const char* metaModuleSource =
"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"
" if (!(source is String)) Fiber.abort(\"Source code must be a string.\")\n"
"\n"
@ -17,4 +25,5 @@ static const char* metaModuleSource =
" }\n"
"\n"
" foreign static compile_(source, isExpression, printErrors)\n"
" foreign static getModuleVariables_(module)\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'.