mirror of
https://github.com/wren-lang/wren.git
synced 2026-01-10 13:48:40 +01:00
Tab-completion in REPL based on module variables.
This commit is contained in:
@ -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" }
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
17
test/meta/get_module_variables.wren
Normal file
17
test/meta/get_module_variables.wren
Normal 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
|
||||
3
test/meta/get_module_variables_not_string.wren
Normal file
3
test/meta/get_module_variables_not_string.wren
Normal file
@ -0,0 +1,3 @@
|
||||
import "meta" for Meta
|
||||
|
||||
Meta.getModuleVariables(123) // expect runtime error: Module name must be a string.
|
||||
3
test/meta/get_module_variables_unknown_module.wren
Normal file
3
test/meta/get_module_variables_unknown_module.wren
Normal file
@ -0,0 +1,3 @@
|
||||
import "meta" for Meta
|
||||
|
||||
Meta.getModuleVariables("unknown") // expect runtime error: Could not find a module named 'unknown'.
|
||||
Reference in New Issue
Block a user