From 1b45084c0ba1a6372a06f87f141d42b090b18b1e Mon Sep 17 00:00:00 2001 From: Jason Poon Date: Wed, 18 Nov 2015 12:00:58 -0800 Subject: [PATCH 1/5] Update README.md --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 56f207148d4..67164a2fe6e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ -## Vim +# Vim [![Build Status](https://travis-ci.org/VSCode-Extension/VSCodeVim.svg?branch=master)](https://travis-ci.org/VSCode-Extension/VSCodeVim) -Coming soon to VS Code... +VIM Visual Studio Code. + +## Contributing + +Would be awesome! + + +## License +[MIT](LICENSE.txt) From 636bb301d9c53f8d328ed7da1e8771e35a01dae4 Mon Sep 17 00:00:00 2001 From: Jason Poon Date: Wed, 18 Nov 2015 12:09:52 -0800 Subject: [PATCH 2/5] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 67164a2fe6e..5efa66fdc72 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ -# Vim +# VSCodeVim [![Build Status](https://travis-ci.org/VSCode-Extension/VSCodeVim.svg?branch=master)](https://travis-ci.org/VSCode-Extension/VSCodeVim) -VIM Visual Studio Code. +Vim for Visual Studio Code. *Coming Soon!* :gift: ## Contributing -Would be awesome! - +... would be awesome! Take a look at [Extension API](https://code.visualstudio.com/docs/extensionAPI/overview) on how to get started. ## License [MIT](LICENSE.txt) From 32c1cfd9b436f8ca29a5fd14cbafc629d70b985c Mon Sep 17 00:00:00 2001 From: guillermooo Date: Thu, 19 Nov 2015 19:20:50 +0100 Subject: [PATCH 3/5] edit readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5efa66fdc72..db4cdaf8e86 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# VSCodeVim +[![Build Status](https://travis-ci.org/VSCode-Extension/VSCodeVim.svg?branch=master)](https://travis-ci.org/VSCode-Extension/VSCodeVim)[![Build status](https://ci.appveyor.com/api/projects/status/0t6ljij7g5h0ddx8?svg=true)](https://ci.appveyor.com/project/guillermooo/vim) -[![Build Status](https://travis-ci.org/VSCode-Extension/VSCodeVim.svg?branch=master)](https://travis-ci.org/VSCode-Extension/VSCodeVim) +# VSCodeVim Vim for Visual Studio Code. *Coming Soon!* :gift: From 3e151bfb1623d06c3c8ee50624db85ed45135fdb Mon Sep 17 00:00:00 2001 From: guillermooo Date: Thu, 19 Nov 2015 19:23:13 +0100 Subject: [PATCH 4/5] edit readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a256587de04..e06ec9cb67c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/VSCodeVim/Vim.svg?branch=master)](https://travis-ci.org/VSCodeVim/Vim)[![Build status](https://ci.appveyor.com/api/projects/status/0t6ljij7g5h0ddx8?svg=true)](https://ci.appveyor.com/project/guillermooo/vim) +[![Build Status](https://travis-ci.org/VSCodeVim/Vim.svg?branch=master)](https://travis-ci.org/VSCodeVim/Vim) [![Build status](https://ci.appveyor.com/api/projects/status/0t6ljij7g5h0ddx8?svg=true)](https://ci.appveyor.com/project/guillermooo/vim) # Vim From a765d5a0617f65e94288eadd4ed25971cdd32b79 Mon Sep 17 00:00:00 2001 From: guillermooo Date: Thu, 19 Nov 2015 22:26:32 +0100 Subject: [PATCH 5/5] implement :w! --- src/cmd_line/command_node.ts | 93 +++++++--- src/cmd_line/lexer.ts | 330 +++++++++++++++++------------------ src/cmd_line/main.ts | 38 ++-- src/cmd_line/node.ts | 158 ++++++++--------- src/cmd_line/parser.ts | 162 ++++++++--------- src/cmd_line/scanner.ts | 168 ++++++++++-------- src/cmd_line/subparser.ts | 68 ++++++++ src/cmd_line/subparsers.ts | 12 -- src/cmd_line/token.ts | 40 ++--- test/index.ts | 4 +- test/lexer.test.ts | 178 +++++++++---------- test/parser.test.ts | 54 +++--- test/scanner.test.ts | 96 +++++----- test/subparser.test.ts | 79 +++++++++ 14 files changed, 842 insertions(+), 638 deletions(-) create mode 100644 src/cmd_line/subparser.ts delete mode 100644 src/cmd_line/subparsers.ts create mode 100644 test/subparser.test.ts diff --git a/src/cmd_line/command_node.ts b/src/cmd_line/command_node.ts index 6c411aad4b9..0b6c60c5e9c 100644 --- a/src/cmd_line/command_node.ts +++ b/src/cmd_line/command_node.ts @@ -1,28 +1,79 @@ -import * as vscode from 'vscode'; -import * as node from './node'; -import * as util from '../util'; +import fs = require('fs'); + +import vscode = require('vscode'); + +import node = require('./node'); +import util = require('../util'); + +export interface WriteCommandArguments { + opt? : string; + optValue? : string; + bang? : boolean; + range? : node.LineRange; + file? : string; + append? : boolean; + cmd? : string; +} // // Implements :write // http://vimdoc.sourceforge.net/htmldoc/editing.html#:write // export class WriteCommand implements node.CommandBase { - name : string; - shortName : string; - args : Object; - - constructor(args : Object = null) { - // TODO: implement other arguments. - this.name = 'write'; - this.shortName = 'w'; - this.args = args; - } - - runOn(textEditor : vscode.TextEditor) : void { - if (this.args) { - util.showError("Not implemented."); - return; - } - textEditor.document.save(); - } + name : string; + shortName : string; + args : WriteCommandArguments; + + constructor(args : WriteCommandArguments = {}) { + this.name = 'write'; + this.shortName = 'w'; + this.args = args; + } + + private doSave(textEditor : vscode.TextEditor) { + textEditor.document.save().then( + (ok) => { + if (ok) { + util.showInfo("File saved."); + } else { + util.showError("File not saved."); + } + }, + (e) => util.showError(e) + ); + }; + + runOn(textEditor : vscode.TextEditor) : void { + if (this.args.opt) { + util.showError("Not implemented."); + return; + } else if (this.args.file) { + util.showError("Not implemented."); + return; + } else if (this.args.append) { + util.showError("Not implemented."); + return; + } else if (this.args.cmd) { + util.showError("Not implemented."); + return; + } + + fs.access(textEditor.document.fileName, fs.W_OK, (accessErr) => { + if (accessErr) { + if (this.args.bang) { + fs.chmod(textEditor.document.fileName, 666, (e) => { + if (e) { + util.showError(e.message); + } else { + this.doSave(textEditor); + } + }); + } else { + util.showError(accessErr.message); + } + } else { + this.doSave(textEditor); + } + }); + } } diff --git a/src/cmd_line/lexer.ts b/src/cmd_line/lexer.ts index 2771938dadd..fbb9fdcc91f 100644 --- a/src/cmd_line/lexer.ts +++ b/src/cmd_line/lexer.ts @@ -3,182 +3,182 @@ import {Token, TokenType} from "./token"; // Describes a function that can lex part of a Vim command line. interface LexFunction { - (state: Scanner, tokens: Token[]) : LexFunction; + (state: Scanner, tokens: Token[]) : LexFunction; } export function lex(input : string) : Token[] { - // We use a character scanner as state for the lexer. - var state = new Scanner(input); - var tokens : Token[] = []; - var f : LexFunction = LexerFunctions.lexRange; - while (f) { - // Each lexing function returns the next lexing function or null. - f = f(state, tokens); - } - return tokens; + // We use a character scanner as state for the lexer. + var state = new Scanner(input); + var tokens : Token[] = []; + var f : LexFunction = LexerFunctions.lexRange; + while (f) { + // Each lexing function returns the next lexing function or null. + f = f(state, tokens); + } + return tokens; } function emitToken(type : TokenType, state : Scanner) : Token { - var content = state.emit(); - return (content.length > 0) ? new Token(type, content) : null; + var content = state.emit(); + return (content.length > 0) ? new Token(type, content) : null; } module LexerFunctions { - // Starts lexing a Vim command line and delegates on other lexer functions as needed. - export function lexRange(state : Scanner, tokens : Token[]): LexFunction { - while (true) { - if (state.isAtEof) { - break; - } - var c = state.next(); - switch (c) { - case ",": - tokens.push(emitToken(TokenType.Comma, state)); - continue; - case "%": - tokens.push(emitToken(TokenType.Percent, state)); - continue; - case "$": - tokens.push(emitToken(TokenType.Dollar, state)); - continue; - case ".": - tokens.push(emitToken(TokenType.Dot, state)); - continue; - case "/": - return lexForwardSearch; - case "?": - return lexReverseSearch; - case "0": - case "1": - case "2": - case "3": - case "4": - case "5": - case "6": - case "7": - case "8": - case "9": - return lexLineRef; - case "+": - tokens.push(emitToken(TokenType.Plus, state)); - continue; - case "-": - tokens.push(emitToken(TokenType.Minus, state)); - continue; - default: - return lexCommand; - } - } - return null; - } + // Starts lexing a Vim command line and delegates on other lexer functions as needed. + export function lexRange(state : Scanner, tokens : Token[]): LexFunction { + while (true) { + if (state.isAtEof) { + break; + } + var c = state.next(); + switch (c) { + case ",": + tokens.push(emitToken(TokenType.Comma, state)); + continue; + case "%": + tokens.push(emitToken(TokenType.Percent, state)); + continue; + case "$": + tokens.push(emitToken(TokenType.Dollar, state)); + continue; + case ".": + tokens.push(emitToken(TokenType.Dot, state)); + continue; + case "/": + return lexForwardSearch; + case "?": + return lexReverseSearch; + case "0": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + return lexLineRef; + case "+": + tokens.push(emitToken(TokenType.Plus, state)); + continue; + case "-": + tokens.push(emitToken(TokenType.Minus, state)); + continue; + default: + return lexCommand; + } + } + return null; + } - function lexLineRef(state : Scanner, tokens : Token[]): LexFunction { - // The first digit has already been lexed. - while (true) { - if (state.isAtEof) { - tokens.push(emitToken(TokenType.LineNumber, state)); - return null; - } - var c = state.next(); - switch (c) { - case "0": - case "1": - case "2": - case "3": - case "4": - case "5": - case "6": - case "7": - case "8": - case "9": - continue; - default: - state.backup(); - tokens.push(emitToken(TokenType.LineNumber, state)); - return lexRange; - } - } - return null; - } + function lexLineRef(state : Scanner, tokens : Token[]): LexFunction { + // The first digit has already been lexed. + while (true) { + if (state.isAtEof) { + tokens.push(emitToken(TokenType.LineNumber, state)); + return null; + } + var c = state.next(); + switch (c) { + case "0": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + continue; + default: + state.backup(); + tokens.push(emitToken(TokenType.LineNumber, state)); + return lexRange; + } + } + return null; + } - function lexCommand(state : Scanner, tokens : Token[]): LexFunction { - // The first character of the command's name has already been lexed. - while (true) { - if (state.isAtEof) { - tokens.push(emitToken(TokenType.CommandName, state)); - break; - } - var c = state.next(); - var lc = c.toLowerCase(); - if (lc >= "a" && lc <= "z") { - continue; - } else { - state.backup(); - tokens.push(emitToken(TokenType.CommandName, state)); - state.skipWhiteSpace(); - while (!state.isAtEof) { - state.next(); - } - // TODO(guillermooo): We need to parse multiple commands. - var args = emitToken(TokenType.CommandArgs, state); - if (args) { - tokens.push(args); - }; - break; - } - } - return null; - } + function lexCommand(state : Scanner, tokens : Token[]): LexFunction { + // The first character of the command's name has already been lexed. + while (true) { + if (state.isAtEof) { + tokens.push(emitToken(TokenType.CommandName, state)); + break; + } + var c = state.next(); + var lc = c.toLowerCase(); + if (lc >= "a" && lc <= "z") { + continue; + } else { + state.backup(); + tokens.push(emitToken(TokenType.CommandName, state)); + state.skipWhiteSpace(); + while (!state.isAtEof) { + state.next(); + } + // TODO(guillermooo): We need to parse multiple commands. + var args = emitToken(TokenType.CommandArgs, state); + if (args) { + tokens.push(args); + }; + break; + } + } + return null; + } - function lexForwardSearch(state : Scanner, tokens : Token[]): LexFunction { - // The first slash has already been lexed. - state.skip("/"); // XXX: really? - var escaping : boolean; - var searchTerm = ""; - while (!state.isAtEof) { - var c = state.next(); - if (c === "/" && !escaping) { - break; - } - if (c === "\\") { - escaping = true; - continue; - } else { - escaping = false; - } - searchTerm += c !== "\\" ? c : "\\\\"; - } - tokens.push(new Token(TokenType.ForwardSearch, searchTerm)); - state.ignore(); - if (!state.isAtEof) { - state.skip("/"); - }; - return lexRange; - } + function lexForwardSearch(state : Scanner, tokens : Token[]): LexFunction { + // The first slash has already been lexed. + state.skip("/"); // XXX: really? + var escaping : boolean; + var searchTerm = ""; + while (!state.isAtEof) { + var c = state.next(); + if (c === "/" && !escaping) { + break; + } + if (c === "\\") { + escaping = true; + continue; + } else { + escaping = false; + } + searchTerm += c !== "\\" ? c : "\\\\"; + } + tokens.push(new Token(TokenType.ForwardSearch, searchTerm)); + state.ignore(); + if (!state.isAtEof) { + state.skip("/"); + }; + return lexRange; + } - function lexReverseSearch(state : Scanner, tokens : Token[]): LexFunction { - // The first question mark has already been lexed. - state.skip("?"); // XXX: really? - var escaping : boolean; - var searchTerm = ""; - while (!state.isAtEof) { - var c = state.next(); - if (c === "?" && !escaping) { - break; - } - if (c === "\\") { - escaping = true; - continue; - } else { - escaping = false; - } - searchTerm += c !== "\\" ? c : "\\\\"; - } - tokens.push(new Token(TokenType.ReverseSearch, searchTerm)); - state.ignore(); - if (!state.isAtEof) { - state.skip("?"); - } - return lexRange; - } + function lexReverseSearch(state : Scanner, tokens : Token[]): LexFunction { + // The first question mark has already been lexed. + state.skip("?"); // XXX: really? + var escaping : boolean; + var searchTerm = ""; + while (!state.isAtEof) { + var c = state.next(); + if (c === "?" && !escaping) { + break; + } + if (c === "\\") { + escaping = true; + continue; + } else { + escaping = false; + } + searchTerm += c !== "\\" ? c : "\\\\"; + } + tokens.push(new Token(TokenType.ReverseSearch, searchTerm)); + state.ignore(); + if (!state.isAtEof) { + state.skip("?"); + } + return lexRange; + } } diff --git a/src/cmd_line/main.ts b/src/cmd_line/main.ts index d94553e0f2f..fe4fdd4d231 100644 --- a/src/cmd_line/main.ts +++ b/src/cmd_line/main.ts @@ -4,27 +4,27 @@ import * as util from "../util"; // Shows the vim command line. export function showCmdLine(initialText = "") { - const options : vscode.InputBoxOptions = { - prompt: "Vim command line", - value: initialText - }; - vscode.window.showInputBox(options).then( - runCmdLine, - vscode.window.showErrorMessage - ); + const options : vscode.InputBoxOptions = { + prompt: "Vim command line", + value: initialText + }; + vscode.window.showInputBox(options).then( + runCmdLine, + vscode.window.showErrorMessage + ); } function runCmdLine(s : string) : void { - try { - var cmd = parser.parse(s); - } catch (e) { - util.showInfo(e); - return; - } + try { + var cmd = parser.parse(s); + } catch (e) { + util.showInfo(e); + return; + } - if (cmd.isEmpty) { - vscode.window.showInformationMessage("empty cmdline"); - } else { - cmd.runOn(vscode.window.activeTextEditor); - } + if (cmd.isEmpty) { + vscode.window.showInformationMessage("empty cmdline"); + } else { + cmd.runOn(vscode.window.activeTextEditor); + } } diff --git a/src/cmd_line/node.ts b/src/cmd_line/node.ts index ea1f635b1a1..30b9cfcee6a 100644 --- a/src/cmd_line/node.ts +++ b/src/cmd_line/node.ts @@ -3,101 +3,101 @@ import * as token from "./token"; export * from "./command_node"; export class LineRange { - left : token.Token[]; - separator : token.Token; - right : token.Token[]; + left : token.Token[]; + separator : token.Token; + right : token.Token[]; - constructor() { - this.left = []; - this.right = []; - } + constructor() { + this.left = []; + this.right = []; + } - addToken(tok : token.Token) : void { - if (tok.type === token.TokenType.Comma) { - this.separator = tok; - return; - } + addToken(tok : token.Token) : void { + if (tok.type === token.TokenType.Comma) { + this.separator = tok; + return; + } - if (!this.separator) { - if (this.left.length > 0 && tok.type !== token.TokenType.Offset) { - // XXX: is this always this error? - throw Error("not a Vim command"); - } - this.left.push(tok); - } else { - if (this.right.length > 0 && tok.type !== token.TokenType.Offset) { - // XXX: is this always this error? - throw Error("not a Vim command"); - } - this.right.push(tok); - } - } + if (!this.separator) { + if (this.left.length > 0 && tok.type !== token.TokenType.Offset) { + // XXX: is this always this error? + throw Error("not a Vim command"); + } + this.left.push(tok); + } else { + if (this.right.length > 0 && tok.type !== token.TokenType.Offset) { + // XXX: is this always this error? + throw Error("not a Vim command"); + } + this.right.push(tok); + } + } - get isEmpty() : boolean { - return this.left.length === 0 && this.right.length === 0 && !this.separator; - } + get isEmpty() : boolean { + return this.left.length === 0 && this.right.length === 0 && !this.separator; + } - toString() : string { - return this.left.toString() + this.separator.content + this.right.toString(); - } + toString() : string { + return this.left.toString() + this.separator.content + this.right.toString(); + } - runOn(document : vscode.TextEditor) : void { - if (this.isEmpty) { - return; - } - var lineRef = this.right.length === 0 ? this.left : this.right; - var pos = this.lineRefToPosition(document, lineRef); - document.selection = new vscode.Selection(pos, pos); - } + runOn(document : vscode.TextEditor) : void { + if (this.isEmpty) { + return; + } + var lineRef = this.right.length === 0 ? this.left : this.right; + var pos = this.lineRefToPosition(document, lineRef); + document.selection = new vscode.Selection(pos, pos); + } - lineRefToPosition(doc : vscode.TextEditor, toks : token.Token[]) : vscode.Position { - var first = toks[0]; - switch (first.type) { - case token.TokenType.Dollar: - case token.TokenType.Percent: - return new vscode.Position(doc.document.lineCount, 0); - case token.TokenType.Dot: - return new vscode.Position(doc.selection.active.line, 0); - case token.TokenType.LineNumber: - var line = Number.parseInt(first.content); - line = Math.max(0, line - 1); - line = Math.min(doc.document.lineCount, line); - return new vscode.Position(line, 0); - default: - throw new Error("not implemented"); - } - } + lineRefToPosition(doc : vscode.TextEditor, toks : token.Token[]) : vscode.Position { + var first = toks[0]; + switch (first.type) { + case token.TokenType.Dollar: + case token.TokenType.Percent: + return new vscode.Position(doc.document.lineCount, 0); + case token.TokenType.Dot: + return new vscode.Position(doc.selection.active.line, 0); + case token.TokenType.LineNumber: + var line = Number.parseInt(first.content); + line = Math.max(0, line - 1); + line = Math.min(doc.document.lineCount, line); + return new vscode.Position(line, 0); + default: + throw new Error("not implemented"); + } + } } export class CommandLine { - range : LineRange; - command : CommandBase; + range : LineRange; + command : CommandBase; - constructor() { - this.range = new LineRange(); - } + constructor() { + this.range = new LineRange(); + } - get isEmpty() : boolean { - return this.range.isEmpty && !this.command; - } + get isEmpty() : boolean { + return this.range.isEmpty && !this.command; + } - toString() : string { - return ":" + this.range.toString() + " " + this.command.toString(); - } + toString() : string { + return ":" + this.range.toString() + " " + this.command.toString(); + } - runOn(document : vscode.TextEditor) : void { - if (!this.command) { - this.range.runOn(document); - return; - } + runOn(document : vscode.TextEditor) : void { + if (!this.command) { + this.range.runOn(document); + return; + } - // TODO: calc range - this.command.runOn(document); - } + // TODO: calc range + this.command.runOn(document); + } } export interface CommandBase { - name : string; - shortName : string; - runOn(textEditor : vscode.TextEditor) : void; + name : string; + shortName : string; + runOn(textEditor : vscode.TextEditor) : void; } diff --git a/src/cmd_line/parser.ts b/src/cmd_line/parser.ts index 12428c5d52c..f741c9fc813 100644 --- a/src/cmd_line/parser.ts +++ b/src/cmd_line/parser.ts @@ -1,102 +1,102 @@ -import * as token from "./token"; -import * as node from "./node"; -import * as lexer from "./lexer"; -import {commandParsers} from "./subparsers"; +import * as token from './token'; +import * as node from './node'; +import * as lexer from './lexer'; +import {commandParsers} from './subparser'; interface ParseFunction { - (state : ParserState, command : node.CommandLine) : ParseFunction; + (state : ParserState, command : node.CommandLine) : ParseFunction; } export function parse(input : string) : node.CommandLine { - var cmd = new node.CommandLine(); - var f : ParseFunction = parseLineRange; - let state : ParserState = new ParserState(input); - while (f) { - f = f(state, cmd); - } - return cmd; + var cmd = new node.CommandLine(); + var f : ParseFunction = parseLineRange; + let state : ParserState = new ParserState(input); + while (f) { + f = f(state, cmd); + } + return cmd; } function parseLineRange(state : ParserState, commandLine : node.CommandLine) : ParseFunction { - while (true) { - let tok = state.next(); - switch (tok.type) { - case token.TokenType.Eof: - return null; - case token.TokenType.Dot: - case token.TokenType.Dollar: - case token.TokenType.Percent: - case token.TokenType.Comma: - case token.TokenType.LineNumber: - commandLine.range.addToken(tok); - continue; - case token.TokenType.CommandName: - state.backup(); - return parseCommand; - // commandLine.command = new node.CommandLineCommand(tok.content, null); - // continue; - default: - console.warn("skipping token " + "Token(" + tok.type + ",{" + tok.content + "})"); - return null; - } - } + while (true) { + let tok = state.next(); + switch (tok.type) { + case token.TokenType.Eof: + return null; + case token.TokenType.Dot: + case token.TokenType.Dollar: + case token.TokenType.Percent: + case token.TokenType.Comma: + case token.TokenType.LineNumber: + commandLine.range.addToken(tok); + continue; + case token.TokenType.CommandName: + state.backup(); + return parseCommand; + // commandLine.command = new node.CommandLineCommand(tok.content, null); + // continue; + default: + console.warn("skipping token " + "Token(" + tok.type + ",{" + tok.content + "})"); + return null; + } + } } function parseCommand(state : ParserState, commandLine : node.CommandLine) : ParseFunction { - while (!state.isAtEof) { - var tok = state.next(); - switch (tok.type) { - case token.TokenType.CommandName: - var commandParser = commandParsers[tok.content]; - if (!commandParser) { - throw new Error("not implemented or not a valid command"); - } - // TODO: Pass the args, but keep in mind there could be multiple - // commands, not just one. - var argsTok = state.next(); - var args = argsTok.type === token.TokenType.CommandArgs ? argsTok.content : null; - commandLine.command = commandParser(args); - return null; - default: - throw new Error("not implemented"); - } - } - if (!state.isAtEof) { - state.backup(); - return parseCommand; - } else { - return null; - } + while (!state.isAtEof) { + var tok = state.next(); + switch (tok.type) { + case token.TokenType.CommandName: + var commandParser = commandParsers[tok.content]; + if (!commandParser) { + throw new Error("not implemented or not a valid command"); + } + // TODO: Pass the args, but keep in mind there could be multiple + // commands, not just one. + var argsTok = state.next(); + var args = argsTok.type === token.TokenType.CommandArgs ? argsTok.content : null; + commandLine.command = commandParser(args); + return null; + default: + throw new Error("not implemented"); + } + } + if (!state.isAtEof) { + state.backup(); + return parseCommand; + } else { + return null; + } } // Keeps track of parsing state. class ParserState { - tokens : token.Token[] = []; - pos : number = 0; + tokens : token.Token[] = []; + pos : number = 0; - constructor(input : string) { - this.lex(input); - } + constructor(input : string) { + this.lex(input); + } - lex(input : string) { - this.tokens = lexer.lex(input); - } + lex(input : string) { + this.tokens = lexer.lex(input); + } - next() : token.Token { - if (this.pos >= this.tokens.length) { - this.pos = this.tokens.length; - return new token.Token(token.TokenType.Eof, "__EOF__"); - } - let tok = this.tokens[this.pos]; - this.pos++; - return tok; - } + next() : token.Token { + if (this.pos >= this.tokens.length) { + this.pos = this.tokens.length; + return new token.Token(token.TokenType.Eof, '__EOF__'); + } + let tok = this.tokens[this.pos]; + this.pos++; + return tok; + } - backup() : void { - this.pos--; - } + backup() : void { + this.pos--; + } - get isAtEof() { - return this.pos >= this.tokens.length; // XXX the last token is TokenEof; is this correct? - } + get isAtEof() { + return this.pos >= this.tokens.length; + } } diff --git a/src/cmd_line/scanner.ts b/src/cmd_line/scanner.ts index b7f893bc69a..49bd024d489 100644 --- a/src/cmd_line/scanner.ts +++ b/src/cmd_line/scanner.ts @@ -1,88 +1,106 @@ // Provides state and behavior to scan an input string character by character. export class Scanner { - static EOF : string = "__EOF__"; - start : number = 0; - pos : number = 0; - input : string; + static EOF : string = "__EOF__"; + start : number = 0; + pos : number = 0; + input : string; - constructor(input : string) { - this.input = input; - } + constructor(input : string) { + this.input = input; + } - // Returns the next character in the input, or EOF. - next() : string { - if (this.isAtEof) { - this.pos = this.input.length; - return Scanner.EOF; - } - let c = this.input[this.pos]; - this.pos++; - return c; - } + // Returns the next character in the input, or EOF. + next() : string { + if (this.isAtEof) { + this.pos = this.input.length; + return Scanner.EOF; + } + let c = this.input[this.pos]; + this.pos++; + return c; + } - // Returns whether we've reached EOF. - get isAtEof() : boolean { - return this.pos >= this.input.length; - } + // Returns whether we've reached EOF. + get isAtEof() : boolean { + return this.pos >= this.input.length; + } - // Ignores the span of text between the current start and the current position. - ignore() : void { - this.start = this.pos; - } + // Ignores the span of text between the current start and the current position. + ignore() : void { + this.start = this.pos; + } - // Returns the span of text between the current start and the current position. - emit() : string { - let s = this.input.substring(this.start, this.pos); - this.ignore(); - return s; - } + // Returns the span of text between the current start and the current position. + emit() : string { + let s = this.input.substring(this.start, this.pos); + this.ignore(); + return s; + } - backup(): void { - this.pos--; - } + backup(): void { + this.pos--; + } - // skips over c and ignores the text span - skip(c : string) : void { - var s = this.next(); - while (!this.isAtEof) { - if (s !== c) { - break; - } - s = this.next(); - } - if (!this.isAtEof) { - this.backup(); - } - this.ignore(); - } + // skips over c and ignores the text span + skip(c : string) : void { + if (this.isAtEof) { + return; + } + var s = this.next(); + while (!this.isAtEof) { + if (s !== c) { + break; + } + s = this.next(); + } + this.backup(); + this.ignore(); + } - // skips text while any of chars matches and ignores the text span - skipRun(...chars : string[]) : void { - while (!this.isAtEof) { - var c = this.next(); - if (chars.indexOf(c) === -1) { - break; - } - } - if (!this.isAtEof) { - this.backup(); - } - this.ignore(); - } + // skips text while any of chars matches and ignores the text span + skipRun(...chars : string[]) : void { + if (this.isAtEof) { + return; + } + while (!this.isAtEof) { + var c = this.next(); + if (chars.indexOf(c) === -1) { + break; + } + } + this.backup(); + this.ignore(); + } - // skips over whitespace (tab, space) and ignores the text span - skipWhiteSpace(): void { - while (true) { - var c = this.next(); - if (c === " " || c === "\t") { - continue; - } - break; - } - if (!this.isAtEof) { - this.backup(); - } - this.ignore(); - } + // skips over whitespace (tab, space) and ignores the text span + skipWhiteSpace(): void { + if (this.isAtEof) { + return; + } + while (true) { + var c = this.next(); + if (c === " " || c === "\t") { + continue; + } + break; + } + this.backup(); + this.ignore(); + } + + expect(value : string) : void { + if (!this.input.substring(this.pos).startsWith(value)) { + throw new Error("Unexpected character."); + } + this.pos += value.length; + } + + expectOneOf(...values : string[]) : void { + let match = values.filter(s => this.input.substr(this.pos).startsWith(s)); + if (match.length !== 1) { + throw new Error("Unexpected character."); + } + this.pos += match[0].length; + } } diff --git a/src/cmd_line/subparser.ts b/src/cmd_line/subparser.ts new file mode 100644 index 00000000000..5bc93e1ad6a --- /dev/null +++ b/src/cmd_line/subparser.ts @@ -0,0 +1,68 @@ +import * as node from "./node"; +import * as command_node from './command_node'; +import {Scanner} from './scanner'; + +// maps command names to parsers for said commands. +export const commandParsers = { + w: parseWriteCommandArgs, + write: parseWriteCommandArgs +}; + +function parseWriteCommandArgs(args : string) : node.WriteCommand { + if (!args) { + return new node.WriteCommand(); + } + var scannedArgs : command_node.WriteCommandArguments = {}; + var scanner = new Scanner(args); + while (true) { + scanner.skipWhiteSpace(); + if (scanner.isAtEof) { + break; + } + let c = scanner.next(); + switch (c) { + case '!': + if (scanner.start > 0) { + // :write !cmd + scanner.ignore(); + while (!scanner.isAtEof) { + scanner.next(); + } + // vim ignores silently if no command after :w ! + scannedArgs.cmd = scanner.emit().trim() || undefined; + continue; + } + // :write! + scannedArgs.bang = true; + scanner.ignore(); + continue; + case '+': + // :write ++opt=value + scanner.expect('+'); + scanner.ignore(); + scanner.expectOneOf('bin', 'nobin', 'ff', 'enc'); + scannedArgs.opt = scanner.emit(); + scanner.expect('='); + scanner.ignore(); + while (!scanner.isAtEof) { + let c = scanner.next(); + if (c !== ' ' || c !== '\t') { + continue; + } + scanner.backup(); + continue; + } + let value = scanner.emit(); + if (!value) { + throw new Error("Expected value for option."); + } + scannedArgs.optValue = value; + continue; + default: + throw new Error("Not implemented"); + } + } + // TODO: actually parse arguments. + // ++bin ++nobin ++ff ++enc =VALUE + return new node.WriteCommand(scannedArgs); +} diff --git a/src/cmd_line/subparsers.ts b/src/cmd_line/subparsers.ts deleted file mode 100644 index 08e4f63cb11..00000000000 --- a/src/cmd_line/subparsers.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as node from "./node"; - -// maps command names to parsers for said commands. -export const commandParsers = { - w: parseWriteCommandArgs, - write: parseWriteCommandArgs -}; - -export function parseWriteCommandArgs(args : string = null) { - // TODO: actually parse arguments. - return new node.WriteCommand(args ? args : null); -} diff --git a/src/cmd_line/token.ts b/src/cmd_line/token.ts index 6df351fe9d9..b66fbc79928 100644 --- a/src/cmd_line/token.ts +++ b/src/cmd_line/token.ts @@ -1,28 +1,28 @@ // Tokens for the Vim command line. export enum TokenType { - Unknown, - Eof, - LineNumber, - Dot, - Dollar, - Percent, - Comma, - Plus, - Minus, - CommandName, - CommandArgs, - ForwardSearch, - ReverseSearch, - Offset + Unknown, + Eof, + LineNumber, + Dot, + Dollar, + Percent, + Comma, + Plus, + Minus, + CommandName, + CommandArgs, + ForwardSearch, + ReverseSearch, + Offset } export class Token { - type : TokenType; - content : string; + type : TokenType; + content : string; - constructor(type : TokenType, content : string) { - this.type = type; - this.content = content; - } + constructor(type : TokenType, content : string) { + this.type = type; + this.content = content; + } } diff --git a/test/index.ts b/test/index.ts index b6c9dadec2a..5e827bb072b 100644 --- a/test/index.ts +++ b/test/index.ts @@ -15,8 +15,8 @@ var testRunner = require('vscode/lib/testrunner'); // You can directly control Mocha options by uncommenting the following lines // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info testRunner.configure({ - ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - useColors: true // colored output from test results + ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: true // colored output from test results }); module.exports = testRunner; diff --git a/test/lexer.test.ts b/test/lexer.test.ts index ef57596732c..03fb16680f2 100644 --- a/test/lexer.test.ts +++ b/test/lexer.test.ts @@ -5,92 +5,92 @@ import {Token, TokenType} from '../src/cmd_line/token' suite("Cmd line tests - lexing", () => { - test("can lex empty string", () => { - var tokens = lexer.lex(""); - assert.equal(tokens.length, 0); - }); - - test("can lex comma", () => { - var tokens = lexer.lex(","); - assert.equal(tokens[0].content, new Token(TokenType.Comma, ',').content); - }); - - test("can lex percent", () => { - var tokens = lexer.lex("%"); - assert.equal(tokens[0].content, new Token(TokenType.Percent, '%').content); - }); - - test("can lex dollar", () => { - var tokens = lexer.lex("$"); - assert.equal(tokens[0].content, new Token(TokenType.Dollar, '$').content); - }); - - test("can lex dot", () => { - var tokens = lexer.lex("."); - assert.equal(tokens[0].content, new Token(TokenType.Dot, '.').content); - }); - - test("can lex one number", () => { - var tokens = lexer.lex("1"); - assert.equal(tokens[0].content, new Token(TokenType.LineNumber, "1").content); - }); - - test("can lex longer number", () => { - var tokens = lexer.lex("100"); - assert.equal(tokens[0].content, new Token(TokenType.LineNumber, "100").content); - }); - - test("can lex plus", () => { - var tokens = lexer.lex("+"); - assert.equal(tokens[0].content, new Token(TokenType.Plus, '+').content); - }); - - test("can lex minus", () => { - var tokens = lexer.lex("-"); - assert.equal(tokens[0].content, new Token(TokenType.Minus, '-').content); - }); - - test("can lex forward search", () => { - var tokens = lexer.lex("/horses/"); - assert.equal(tokens[0].content, new Token(TokenType.ForwardSearch, "horses").content); - }); - - test("can lex forward search escaping", () => { - var tokens = lexer.lex("/hor\\/ses/"); - assert.equal(tokens[0].content, new Token(TokenType.ForwardSearch, "hor/ses").content); - }); - - test("can lex reverse search", () => { - var tokens = lexer.lex("?worms?"); - assert.equal(tokens[0].content, new Token(TokenType.ReverseSearch, "worms").content); - }); - - test("can lex reverse search escaping", () => { - var tokens = lexer.lex("?wor\\?ms?"); - assert.equal(tokens[0].content, new Token(TokenType.ReverseSearch, "wor?ms").content); - }); - - test("can lex command name", () => { - var tokens = lexer.lex("w"); - assert.equal(tokens[0].content, new Token(TokenType.CommandName, "w").content); - }); - - test("can lex command args", () => { - var tokens = lexer.lex("w something"); - assert.equal(tokens[0].content, new Token(TokenType.CommandName, "w").content); - assert.equal(tokens[1].content, new Token(TokenType.CommandArgs, "something").content); - }); - - test("can lex long command name and args", () => { - var tokens = lexer.lex("write12 something here"); - assert.equal(tokens[0].content, new Token(TokenType.CommandName, "write").content); - assert.equal(tokens[1].content, new Token(TokenType.CommandArgs, "12 something here").content); - }); - - test("can lex left and right line refs", () => { - var tokens = lexer.lex("20,30"); - assert.equal(tokens[0].content, new Token(TokenType.LineNumber, "20").content); - assert.equal(tokens[1].content, new Token(TokenType.LineNumber, ",").content); - assert.equal(tokens[2].content, new Token(TokenType.LineNumber, "30").content); - }); -}); \ No newline at end of file + test("can lex empty string", () => { + var tokens = lexer.lex(""); + assert.equal(tokens.length, 0); + }); + + test("can lex comma", () => { + var tokens = lexer.lex(","); + assert.equal(tokens[0].content, new Token(TokenType.Comma, ',').content); + }); + + test("can lex percent", () => { + var tokens = lexer.lex("%"); + assert.equal(tokens[0].content, new Token(TokenType.Percent, '%').content); + }); + + test("can lex dollar", () => { + var tokens = lexer.lex("$"); + assert.equal(tokens[0].content, new Token(TokenType.Dollar, '$').content); + }); + + test("can lex dot", () => { + var tokens = lexer.lex("."); + assert.equal(tokens[0].content, new Token(TokenType.Dot, '.').content); + }); + + test("can lex one number", () => { + var tokens = lexer.lex("1"); + assert.equal(tokens[0].content, new Token(TokenType.LineNumber, "1").content); + }); + + test("can lex longer number", () => { + var tokens = lexer.lex("100"); + assert.equal(tokens[0].content, new Token(TokenType.LineNumber, "100").content); + }); + + test("can lex plus", () => { + var tokens = lexer.lex("+"); + assert.equal(tokens[0].content, new Token(TokenType.Plus, '+').content); + }); + + test("can lex minus", () => { + var tokens = lexer.lex("-"); + assert.equal(tokens[0].content, new Token(TokenType.Minus, '-').content); + }); + + test("can lex forward search", () => { + var tokens = lexer.lex("/horses/"); + assert.equal(tokens[0].content, new Token(TokenType.ForwardSearch, "horses").content); + }); + + test("can lex forward search escaping", () => { + var tokens = lexer.lex("/hor\\/ses/"); + assert.equal(tokens[0].content, new Token(TokenType.ForwardSearch, "hor/ses").content); + }); + + test("can lex reverse search", () => { + var tokens = lexer.lex("?worms?"); + assert.equal(tokens[0].content, new Token(TokenType.ReverseSearch, "worms").content); + }); + + test("can lex reverse search escaping", () => { + var tokens = lexer.lex("?wor\\?ms?"); + assert.equal(tokens[0].content, new Token(TokenType.ReverseSearch, "wor?ms").content); + }); + + test("can lex command name", () => { + var tokens = lexer.lex("w"); + assert.equal(tokens[0].content, new Token(TokenType.CommandName, "w").content); + }); + + test("can lex command args", () => { + var tokens = lexer.lex("w something"); + assert.equal(tokens[0].content, new Token(TokenType.CommandName, "w").content); + assert.equal(tokens[1].content, new Token(TokenType.CommandArgs, "something").content); + }); + + test("can lex long command name and args", () => { + var tokens = lexer.lex("write12 something here"); + assert.equal(tokens[0].content, new Token(TokenType.CommandName, "write").content); + assert.equal(tokens[1].content, new Token(TokenType.CommandArgs, "12 something here").content); + }); + + test("can lex left and right line refs", () => { + var tokens = lexer.lex("20,30"); + assert.equal(tokens[0].content, new Token(TokenType.LineNumber, "20").content); + assert.equal(tokens[1].content, new Token(TokenType.LineNumber, ",").content); + assert.equal(tokens[2].content, new Token(TokenType.LineNumber, "30").content); + }); +}); diff --git a/test/parser.test.ts b/test/parser.test.ts index 2a3e39ac2a9..909baa4c60c 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -6,36 +6,36 @@ import * as token from '../src/cmd_line/token'; suite("Cmd line tests - parser", () => { - test("can parse empty string", () => { - var cmd = parser.parse(""); - assert.ok(cmd.isEmpty); - }); + test("can parse empty string", () => { + var cmd = parser.parse(""); + assert.ok(cmd.isEmpty); + }); - // TODO: Range tests follow -- should prolly create a suite for this - test("can parse left - dot", () => { - var cmd : node.CommandLine = parser.parse("."); - assert.equal(cmd.range.left[0].type, token.TokenType.Dot); - }); + // TODO: Range tests follow -- should prolly create a suite for this + test("can parse left - dot", () => { + var cmd : node.CommandLine = parser.parse("."); + assert.equal(cmd.range.left[0].type, token.TokenType.Dot); + }); - test("can parse left - dollar", () => { - var cmd : node.CommandLine = parser.parse("$"); - assert.equal(cmd.range.left[0].type, token.TokenType.Dollar); - }); + test("can parse left - dollar", () => { + var cmd : node.CommandLine = parser.parse("$"); + assert.equal(cmd.range.left[0].type, token.TokenType.Dollar); + }); - test("can parse left - percent", () => { - var cmd : node.CommandLine = parser.parse("%"); - assert.equal(cmd.range.left[0].type, token.TokenType.Percent); - }); + test("can parse left - percent", () => { + var cmd : node.CommandLine = parser.parse("%"); + assert.equal(cmd.range.left[0].type, token.TokenType.Percent); + }); - test("can parse separator - comma", () => { - var cmd : node.CommandLine = parser.parse(","); - assert.equal(cmd.range.separator.type, token.TokenType.Comma); - }); + test("can parse separator - comma", () => { + var cmd : node.CommandLine = parser.parse(","); + assert.equal(cmd.range.separator.type, token.TokenType.Comma); + }); - test("can parse right - dollar", () => { - var cmd : node.CommandLine = parser.parse(",$"); - assert.equal(cmd.range.left.length, 0); - assert.equal(cmd.range.right.length, 1); - assert.equal(cmd.range.right[0].type, token.TokenType.Dollar, "unexpected token"); - }); + test("can parse right - dollar", () => { + var cmd : node.CommandLine = parser.parse(",$"); + assert.equal(cmd.range.left.length, 0); + assert.equal(cmd.range.right.length, 1); + assert.equal(cmd.range.right[0].type, token.TokenType.Dollar, "unexpected token"); + }); }); diff --git a/test/scanner.test.ts b/test/scanner.test.ts index 76f2e8a01c0..f41dd307c51 100644 --- a/test/scanner.test.ts +++ b/test/scanner.test.ts @@ -4,54 +4,54 @@ import * as lexerState from '../src/cmd_line/scanner' suite("Cmd line tests - lexer state", () => { - test("can init lexer state", () => { - var state = new lexerState.Scanner("dog"); - assert.equal(state.input, "dog"); - }); - - test("can detect EOF with empty input", () => { - var state = new lexerState.Scanner(""); - assert.ok(state.isAtEof); - }); + test("can init lexer state", () => { + var state = new lexerState.Scanner("dog"); + assert.equal(state.input, "dog"); + }); + + test("can detect EOF with empty input", () => { + var state = new lexerState.Scanner(""); + assert.ok(state.isAtEof); + }); - test("next() returns EOF at EOF", () => { - var state = new lexerState.Scanner(""); - assert.equal(state.next(), lexerState.Scanner.EOF); - assert.equal(state.next(), lexerState.Scanner.EOF); - assert.equal(state.next(), lexerState.Scanner.EOF); - }); + test("next() returns EOF at EOF", () => { + var state = new lexerState.Scanner(""); + assert.equal(state.next(), lexerState.Scanner.EOF); + assert.equal(state.next(), lexerState.Scanner.EOF); + assert.equal(state.next(), lexerState.Scanner.EOF); + }); - test("next() can scan", () => { - var state = new lexerState.Scanner("dog"); - assert.equal(state.next(), "d"); - assert.equal(state.next(), "o"); - assert.equal(state.next(), "g") - assert.equal(state.next(), lexerState.Scanner.EOF); - }); - - test("can emit", () => { - var state = new lexerState.Scanner("dog cat"); - state.next(); - state.next(); - state.next(); - assert.equal(state.emit(), "dog"); - state.next(); - state.next(); - state.next(); - state.next(); - assert.equal(state.emit(), " cat"); - }); + test("next() can scan", () => { + var state = new lexerState.Scanner("dog"); + assert.equal(state.next(), "d"); + assert.equal(state.next(), "o"); + assert.equal(state.next(), "g") + assert.equal(state.next(), lexerState.Scanner.EOF); + }); + + test("can emit", () => { + var state = new lexerState.Scanner("dog cat"); + state.next(); + state.next(); + state.next(); + assert.equal(state.emit(), "dog"); + state.next(); + state.next(); + state.next(); + state.next(); + assert.equal(state.emit(), " cat"); + }); - test("can ignore", () => { - var state = new lexerState.Scanner("dog cat"); - state.next(); - state.next(); - state.next(); - state.next(); - state.ignore(); - state.next(); - state.next(); - state.next(); - assert.equal(state.emit(), "cat"); - }); -}); \ No newline at end of file + test("can ignore", () => { + var state = new lexerState.Scanner("dog cat"); + state.next(); + state.next(); + state.next(); + state.next(); + state.ignore(); + state.next(); + state.next(); + state.next(); + assert.equal(state.emit(), "cat"); + }); +}); diff --git a/test/subparser.test.ts b/test/subparser.test.ts new file mode 100644 index 00000000000..58ed5c00b5a --- /dev/null +++ b/test/subparser.test.ts @@ -0,0 +1,79 @@ +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; + +import {commandParsers} from '../src/cmd_line/subparser'; +import {WriteCommandArguments} from '../src/cmd_line/command_node'; + +suite("subparsers - :write args", () => { + + test("parsers for :write are set up correctly", () => { + assert.equal(commandParsers.write.name, commandParsers.w.name); + }); + + test("can parse empty args", () => { + // TODO: perhaps we don't need to export this func at all. + // TODO: this func must return args only, not a command? + // TODO: the range must be passed separately, not as arg. + var args = commandParsers.write(""); + assert.equal(args.args.append, undefined); + assert.equal(args.args.bang, undefined); + assert.equal(args.args.cmd, undefined); + assert.equal(args.args.file, undefined); + assert.equal(args.args.opt, undefined); + assert.equal(args.args.optValue, undefined); + assert.equal(args.args.range, undefined); + }); + + test("can parse ++opt", () => { + + var args = commandParsers.write("++enc=foo"); + assert.equal(args.args.append, undefined); + assert.equal(args.args.bang, undefined); + assert.equal(args.args.cmd, undefined); + assert.equal(args.args.file, undefined); + assert.equal(args.args.opt, 'enc'); + assert.equal(args.args.optValue, 'foo'); + assert.equal(args.args.range, undefined); + }); + + test("throws if bad ++opt name", () => { + + assert.throws(() => commandParsers.write("++foo=foo")); + }); + + test("can parse bang", () => { + + var args = commandParsers.write("!"); + assert.equal(args.args.append, undefined); + assert.equal(args.args.bang, true); + assert.equal(args.args.cmd, undefined); + assert.equal(args.args.file, undefined); + assert.equal(args.args.opt, undefined); + assert.equal(args.args.optValue, undefined); + assert.equal(args.args.range, undefined); + }); + + test("can parse ' !cmd'", () => { + + var args = commandParsers.write(" !foo"); + assert.equal(args.args.append, undefined); + assert.equal(args.args.bang, undefined); + assert.equal(args.args.cmd, 'foo'); + assert.equal(args.args.file, undefined); + assert.equal(args.args.opt, undefined); + assert.equal(args.args.optValue, undefined); + assert.equal(args.args.range, undefined); + }); + + test("can parse ' !cmd' when cmd is empty", () => { + + var args = commandParsers.write(" !"); + assert.equal(args.args.append, undefined); + assert.equal(args.args.bang, undefined); + assert.equal(args.args.cmd, undefined); + assert.equal(args.args.file, undefined); + assert.equal(args.args.opt, undefined); + assert.equal(args.args.optValue, undefined); + assert.equal(args.args.range, undefined); + }); +});