From f47e128978a470e05fa381bb68ff8ba245f36962 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Sun, 22 May 2016 15:32:17 -0700 Subject: [PATCH] Tab-completion in REPL based on module variables. --- src/module/repl.wren | 59 ++++++++++++++---- src/module/repl.wren.inc | 60 +++++++++++++++---- src/optional/wren_opt_meta.c | 42 ++++++++++++- src/optional/wren_opt_meta.wren | 9 +++ src/optional/wren_opt_meta.wren.inc | 9 +++ test/meta/get_module_variables.wren | 17 ++++++ .../meta/get_module_variables_not_string.wren | 3 + .../get_module_variables_unknown_module.wren | 3 + 8 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 test/meta/get_module_variables.wren create mode 100644 test/meta/get_module_variables_not_string.wren create mode 100644 test/meta/get_module_variables_unknown_module.wren diff --git a/src/module/repl.wren b/src/module/repl.wren index d0fbe6ac..fe6f078a 100644 --- a/src/module/repl.wren +++ b/src/module/repl.wren @@ -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" } diff --git a/src/module/repl.wren.inc b/src/module/repl.wren.inc index 9af2686a..6a374627 100644 --- a/src/module/repl.wren.inc +++ b/src/module/repl.wren.inc @@ -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" diff --git a/src/optional/wren_opt_meta.c b/src/optional/wren_opt_meta.c index 19f7a045..ccc33116 100644 --- a/src/optional/wren_opt_meta.c +++ b/src/optional/wren_opt_meta.c @@ -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 diff --git a/src/optional/wren_opt_meta.wren b/src/optional/wren_opt_meta.wren index 2660f0f9..f5365ea2 100644 --- a/src/optional/wren_opt_meta.wren +++ b/src/optional/wren_opt_meta.wren @@ -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) } diff --git a/src/optional/wren_opt_meta.wren.inc b/src/optional/wren_opt_meta.wren.inc index 1798636b..526576d8 100644 --- a/src/optional/wren_opt_meta.wren.inc +++ b/src/optional/wren_opt_meta.wren.inc @@ -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"; diff --git a/test/meta/get_module_variables.wren b/test/meta/get_module_variables.wren new file mode 100644 index 00000000..c5ab61b9 --- /dev/null +++ b/test/meta/get_module_variables.wren @@ -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 diff --git a/test/meta/get_module_variables_not_string.wren b/test/meta/get_module_variables_not_string.wren new file mode 100644 index 00000000..2e8f5a4a --- /dev/null +++ b/test/meta/get_module_variables_not_string.wren @@ -0,0 +1,3 @@ +import "meta" for Meta + +Meta.getModuleVariables(123) // expect runtime error: Module name must be a string. diff --git a/test/meta/get_module_variables_unknown_module.wren b/test/meta/get_module_variables_unknown_module.wren new file mode 100644 index 00000000..9d6afa79 --- /dev/null +++ b/test/meta/get_module_variables_unknown_module.wren @@ -0,0 +1,3 @@ +import "meta" for Meta + +Meta.getModuleVariables("unknown") // expect runtime error: Could not find a module named 'unknown'.