mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
o replaced debugger/input package with debugger/commandline
- improved how command template are compiled - verification and tab completion is now more robust - missing repeat groups o debugger - extended PLAYER and MISSILE commands - combined STICK1 and STICK2 into one STICK command
This commit is contained in:
parent
a9b313bbcc
commit
46b8173b7b
26 changed files with 1634 additions and 558 deletions
|
@ -6,8 +6,8 @@ package debugger
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"gopher2600/debugger/commandline"
|
||||
"gopher2600/debugger/console"
|
||||
"gopher2600/debugger/input"
|
||||
"gopher2600/errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -171,7 +171,7 @@ func (bp breakpoints) list() {
|
|||
// & SL 100 HP 0 | X 10
|
||||
//
|
||||
// TODO: more sophisticated breakpoints parser
|
||||
func (bp *breakpoints) parseBreakpoint(tokens *input.Tokens) error {
|
||||
func (bp *breakpoints) parseBreakpoint(tokens *commandline.Tokens) error {
|
||||
andBreaks := false
|
||||
|
||||
// default target of CPU PC. meaning that "BREAK n" will cause a breakpoint
|
||||
|
|
|
@ -2,7 +2,7 @@ package colorterm
|
|||
|
||||
import (
|
||||
"gopher2600/debugger/colorterm/easyterm"
|
||||
"gopher2600/debugger/input"
|
||||
"gopher2600/debugger/console"
|
||||
"os"
|
||||
)
|
||||
|
||||
|
@ -12,7 +12,7 @@ type ColorTerminal struct {
|
|||
|
||||
reader runeReader
|
||||
commandHistory []command
|
||||
tabCompleter *input.TabCompletion
|
||||
tabCompleter console.TabCompleter
|
||||
}
|
||||
|
||||
type command struct {
|
||||
|
@ -41,7 +41,7 @@ func (ct *ColorTerminal) CleanUp() {
|
|||
|
||||
// RegisterTabCompleter adds an implementation of TabCompleter to the
|
||||
// ColorTerminal
|
||||
func (ct *ColorTerminal) RegisterTabCompleter(tc *input.TabCompletion) {
|
||||
func (ct *ColorTerminal) RegisterTabCompleter(tc console.TabCompleter) {
|
||||
ct.tabCompleter = tc
|
||||
}
|
||||
|
||||
|
|
|
@ -65,10 +65,18 @@ func (ct *ColorTerminal) UserRead(input []byte, prompt string, events chan gui.E
|
|||
return inputLen, readRune.err
|
||||
}
|
||||
|
||||
// reset tabcompletion state if anything other than the tab key has
|
||||
// been pressed
|
||||
if readRune.r != easyterm.KeyTab {
|
||||
if ct.tabCompleter != nil {
|
||||
ct.tabCompleter.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
switch readRune.r {
|
||||
case easyterm.KeyTab:
|
||||
if ct.tabCompleter != nil {
|
||||
s := ct.tabCompleter.GuessWord(string(input[:cursorPos]))
|
||||
s := ct.tabCompleter.Complete(string(input[:cursorPos]))
|
||||
|
||||
// the difference in the length of the new input and the old
|
||||
// input
|
||||
|
|
35
debugger/commandline/errors.go
Normal file
35
debugger/commandline/errors.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package commandline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseError is the error type for the ParseCommandTemplate function
|
||||
type ParseError struct {
|
||||
definition string
|
||||
position int
|
||||
underlyingError error
|
||||
}
|
||||
|
||||
func (er ParseError) Error() string {
|
||||
return fmt.Sprintf("parser error: %s", er.underlyingError)
|
||||
}
|
||||
|
||||
// Location returns detailed information about the Error
|
||||
func (er ParseError) Location() string {
|
||||
s := strings.Builder{}
|
||||
s.WriteString(er.definition)
|
||||
s.WriteString("\n")
|
||||
s.WriteString(fmt.Sprintf("%s^", strings.Repeat(" ", er.position)))
|
||||
return s.String()
|
||||
}
|
||||
|
||||
// NewParseError is used to create a new instance of a Error
|
||||
func NewParseError(defn string, position int, underlyingError error) *ParseError {
|
||||
er := new(ParseError)
|
||||
er.definition = defn
|
||||
er.position = position
|
||||
er.underlyingError = underlyingError
|
||||
return er
|
||||
}
|
98
debugger/commandline/nodes.go
Normal file
98
debugger/commandline/nodes.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package commandline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Commands is the root of the command tree
|
||||
//
|
||||
// currently, the top-level of the Commands tree is an array of nodes. each
|
||||
// entry in this array is effectively a branch off a conceptual root-node. with
|
||||
// a bit of work, we could alter the command tree such that the array is a
|
||||
// sequence of branches off an otherwise unused root-node. this would simplify
|
||||
// validation and tab-completion a little bit. as it is though, this is fine
|
||||
// for now.
|
||||
type Commands []*node
|
||||
|
||||
// Len implements Sort package interface
|
||||
func (cmds Commands) Len() int {
|
||||
return len(cmds)
|
||||
}
|
||||
|
||||
// Less implements Sort package interface
|
||||
func (cmds Commands) Less(i int, j int) bool {
|
||||
return cmds[i].tag < cmds[j].tag
|
||||
}
|
||||
|
||||
// Swap implements Sort package interface
|
||||
func (cmds Commands) Swap(i int, j int) {
|
||||
swp := cmds[i]
|
||||
cmds[i] = cmds[j]
|
||||
cmds[j] = swp
|
||||
}
|
||||
|
||||
func (cmds Commands) String() string {
|
||||
s := strings.Builder{}
|
||||
for c := range cmds {
|
||||
s.WriteString(fmt.Sprintf("%v", cmds[c]))
|
||||
s.WriteString("\n")
|
||||
}
|
||||
return strings.TrimRight(s.String(), "\n")
|
||||
}
|
||||
|
||||
type groupType int
|
||||
|
||||
const (
|
||||
groupUndefined groupType = iota
|
||||
groupRoot
|
||||
groupRequired
|
||||
groupOptional
|
||||
)
|
||||
|
||||
type node struct {
|
||||
// tag should always be non-empty
|
||||
tag string
|
||||
|
||||
// group will have the following values:
|
||||
// groupRoot: nodes that are not in an explicit grouping
|
||||
// groupRequired
|
||||
// groupOptional
|
||||
group groupType
|
||||
|
||||
next []*node
|
||||
branch []*node
|
||||
}
|
||||
|
||||
func (n node) String() string {
|
||||
s := strings.Builder{}
|
||||
|
||||
s.WriteString(n.tag)
|
||||
|
||||
if n.next != nil {
|
||||
for i := range n.next {
|
||||
if n.next[i].group == groupRequired {
|
||||
s.WriteString(" [")
|
||||
} else if n.next[i].group == groupOptional {
|
||||
s.WriteString(" (")
|
||||
} else {
|
||||
s.WriteString(" ")
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("%s", n.next[i]))
|
||||
if n.next[i].group == groupRequired {
|
||||
s.WriteString("]")
|
||||
} else if n.next[i].group == groupOptional {
|
||||
s.WriteString(")")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if n.branch != nil {
|
||||
for i := range n.branch {
|
||||
s.WriteString(fmt.Sprintf("|%s", n.branch[i]))
|
||||
}
|
||||
}
|
||||
|
||||
return s.String()
|
||||
}
|
287
debugger/commandline/parser.go
Normal file
287
debugger/commandline/parser.go
Normal file
|
@ -0,0 +1,287 @@
|
|||
package commandline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseCommandTemplate turns a string representation of a command template
|
||||
// into a machine friendly representation
|
||||
//
|
||||
// Syntax
|
||||
// [ a ] required keyword
|
||||
// ( a ) optional keyword
|
||||
// [ a | b | ... ] required selection
|
||||
// ( a | b | ... ) optional selection
|
||||
//
|
||||
// groups can be embedded in one another
|
||||
//
|
||||
// Placeholders
|
||||
// %V numeric value
|
||||
// %I irrational number value
|
||||
// %S string
|
||||
// %F file name
|
||||
// %* allow anything to follow this point
|
||||
//
|
||||
// note that a placeholder will implicitly be treated as a separate token
|
||||
//
|
||||
func ParseCommandTemplate(template []string) (*Commands, error) {
|
||||
cmds := make(Commands, 0, 10)
|
||||
for t := range template {
|
||||
defn := template[t]
|
||||
|
||||
// tidy up spaces in definition string - we don't want more than one
|
||||
// consecutive space
|
||||
defn = strings.Join(strings.Fields(defn), " ")
|
||||
|
||||
// normalise to upper case
|
||||
defn = strings.ToUpper(defn)
|
||||
|
||||
// parse the definition for this command
|
||||
p, d, err := parseDefinition(defn, "")
|
||||
if err != nil {
|
||||
return nil, NewParseError(defn, d, fmt.Errorf("%s: %s", err, defn))
|
||||
}
|
||||
|
||||
// add to list of commands (order doesn't matter at this stage)
|
||||
cmds = append(cmds, p)
|
||||
|
||||
// check that parsing was complete
|
||||
if d < len(defn)-1 {
|
||||
return nil, NewParseError(defn, len(defn), fmt.Errorf("outstanding characters in command definition"))
|
||||
}
|
||||
}
|
||||
|
||||
return &cmds, nil
|
||||
}
|
||||
|
||||
func parseDefinition(defn string, trigger string) (*node, int, error) {
|
||||
// handle special conditions before parsing loop
|
||||
if defn[0] == '(' || defn[0] == '[' {
|
||||
return nil, 0, fmt.Errorf("first argument of a group should not be itself be the start of a group")
|
||||
}
|
||||
|
||||
// working nodes should be initialised with this function
|
||||
newWorkingNode := func() *node {
|
||||
if trigger == "(" {
|
||||
return &node{group: groupOptional}
|
||||
} else if trigger == "[" {
|
||||
return &node{group: groupRequired}
|
||||
} else if trigger == "|" {
|
||||
// group is left unset for the branch trigger. value will be set
|
||||
// once parseDefinition() has returned
|
||||
return &node{}
|
||||
} else if trigger == "" {
|
||||
return &node{group: groupRoot}
|
||||
}
|
||||
|
||||
panic("unknown trigger")
|
||||
}
|
||||
|
||||
wn := newWorkingNode() // working node (attached to the end of the sequence when required)
|
||||
sn := wn // start node (of the sequence)
|
||||
|
||||
addNext := func(nx *node) error {
|
||||
// new node is already in the correct place
|
||||
if sn == nx {
|
||||
wn = newWorkingNode()
|
||||
return nil
|
||||
}
|
||||
|
||||
// do not add nodes that have no content
|
||||
if nx.tag == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sanity check to make sure we're not clobbering an active working
|
||||
// node
|
||||
if wn != nx && wn.tag != "" {
|
||||
return fmt.Errorf("orphaned working node: %s", wn.tag)
|
||||
}
|
||||
|
||||
// create a new next array if necessary, and add new node to the end of
|
||||
// it
|
||||
if sn.next == nil {
|
||||
sn.next = make([]*node, 0)
|
||||
}
|
||||
sn.next = append(sn.next, nx)
|
||||
|
||||
// create new working node
|
||||
wn = newWorkingNode()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
addBranch := func(bx *node) error {
|
||||
// do not add nodes that have no content
|
||||
if bx.tag == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sanity check to make sure we're not clobbering an active working
|
||||
// node
|
||||
if wn != bx && wn.tag != "" {
|
||||
return fmt.Errorf("orphaned working node: %s", wn.tag)
|
||||
}
|
||||
|
||||
// create a new next array if necessary, and add new node to the end of
|
||||
// it
|
||||
if sn.branch == nil {
|
||||
sn.branch = make([]*node, 0)
|
||||
}
|
||||
sn.branch = append(sn.branch, bx)
|
||||
|
||||
// create new working node
|
||||
wn = newWorkingNode()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < len(defn); i++ {
|
||||
switch defn[i] {
|
||||
case '[':
|
||||
err := addNext(wn)
|
||||
if err != nil {
|
||||
return nil, i, err
|
||||
}
|
||||
|
||||
i++
|
||||
ns, e, err := parseDefinition(defn[i:], "[")
|
||||
if err != nil {
|
||||
return nil, i + e, err
|
||||
}
|
||||
ns.group = groupRequired
|
||||
|
||||
err = addNext(ns)
|
||||
if err != nil {
|
||||
return nil, i, err
|
||||
}
|
||||
|
||||
i += e
|
||||
|
||||
case '(':
|
||||
err := addNext(wn)
|
||||
if err != nil {
|
||||
return nil, i, err
|
||||
}
|
||||
|
||||
i++
|
||||
ns, e, err := parseDefinition(defn[i:], "(")
|
||||
if err != nil {
|
||||
return nil, i + e, err
|
||||
}
|
||||
ns.group = groupOptional
|
||||
|
||||
err = addNext(ns)
|
||||
|
||||
if err != nil {
|
||||
return nil, i, err
|
||||
}
|
||||
|
||||
i += e
|
||||
|
||||
case ']':
|
||||
err := addNext(wn)
|
||||
if err != nil {
|
||||
return nil, i, err
|
||||
}
|
||||
|
||||
if trigger == "[" {
|
||||
return sn, i, nil
|
||||
}
|
||||
if trigger == "|" {
|
||||
return sn, i - 1, nil
|
||||
}
|
||||
return nil, i, fmt.Errorf("unexpected ]")
|
||||
|
||||
case ')':
|
||||
err := addNext(wn)
|
||||
if err != nil {
|
||||
return nil, i, err
|
||||
}
|
||||
|
||||
if trigger == "(" {
|
||||
return sn, i, nil
|
||||
}
|
||||
if trigger == "|" {
|
||||
return sn, i - 1, nil
|
||||
}
|
||||
return nil, i, fmt.Errorf("unexpected )")
|
||||
|
||||
case '|':
|
||||
err := addNext(wn)
|
||||
if err != nil {
|
||||
return nil, i, err
|
||||
}
|
||||
|
||||
if trigger == "|" {
|
||||
return sn, i - 1, nil
|
||||
}
|
||||
|
||||
i++
|
||||
|
||||
nb, e, err := parseDefinition(defn[i:], "|")
|
||||
if err != nil {
|
||||
return nil, i + e, err
|
||||
}
|
||||
|
||||
// change group to current group - we don't want any unresolved
|
||||
// instances of groupUndefined
|
||||
nb.group = sn.group
|
||||
|
||||
err = addBranch(nb)
|
||||
if err != nil {
|
||||
return nil, i, err
|
||||
}
|
||||
|
||||
i += e
|
||||
|
||||
case '%':
|
||||
if wn.tag != "" {
|
||||
return nil, i, fmt.Errorf("placeholders cannot be part of a wider string")
|
||||
}
|
||||
|
||||
if i == len(defn)-1 {
|
||||
return nil, i, fmt.Errorf("orphaned placeholder directives not allowed")
|
||||
}
|
||||
|
||||
// add placeholder to working node if it is recognised
|
||||
p := string(defn[i+1])
|
||||
|
||||
if p != "V" && p != "I" && p != "S" && p != "F" && p != "*" && p != "%" {
|
||||
return nil, i, fmt.Errorf("unknown placeholder directive (%s)", wn.tag)
|
||||
}
|
||||
|
||||
wn.tag = fmt.Sprintf("%%%s", p)
|
||||
|
||||
// we've consumed an additional character when retreiving a value
|
||||
// for p
|
||||
i++
|
||||
|
||||
case ' ':
|
||||
// tokens are separated by spaces as well group markers
|
||||
err := addNext(wn)
|
||||
if err != nil {
|
||||
return nil, i, err
|
||||
}
|
||||
|
||||
default:
|
||||
wn.tag += string(defn[i])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// make sure we've added working node to the sequence
|
||||
err := addNext(wn)
|
||||
if err != nil {
|
||||
return nil, len(defn), err
|
||||
}
|
||||
|
||||
// if we reach this point and trigger is non-empty then that implies that
|
||||
// the opening trigger has not been closed correctly
|
||||
if trigger == "[" || trigger == "(" {
|
||||
return nil, len(defn), fmt.Errorf(fmt.Sprintf("unclosed %s group", trigger))
|
||||
}
|
||||
|
||||
return sn, len(defn), nil
|
||||
}
|
164
debugger/commandline/parser_test.go
Normal file
164
debugger/commandline/parser_test.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package commandline_test
|
||||
|
||||
import (
|
||||
"gopher2600/debugger/commandline"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bradleyjkemp/memviz"
|
||||
)
|
||||
|
||||
func expectFailure(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Errorf("expected failure")
|
||||
}
|
||||
}
|
||||
|
||||
func expectSuccess(t *testing.T, err error) bool {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Errorf("%s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// expectEquality compares a template, as passed to ParseCommandTemplate(),
|
||||
// with the String() output of the resulting Commands object. both outputs
|
||||
// should be the same.
|
||||
//
|
||||
// the template is transformed slightly. each entry in the array is joined with
|
||||
// a newline character and also converted to uppercase. this is okay because
|
||||
// we're only really interested in how the groupings and branching is
|
||||
// represented.
|
||||
func expectEquality(t *testing.T, template []string, cmds *commandline.Commands) bool {
|
||||
t.Helper()
|
||||
if strings.ToUpper(strings.Join(template, "\n")) != cmds.String() {
|
||||
t.Errorf("parsed commands do not match template")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// memvizOutput produces a dot file that can be used to identify how elements
|
||||
// in the Commands object are connected
|
||||
func memvizOutput(t *testing.T, filename string, cmds *commandline.Commands) {
|
||||
if len(filename) < 4 || strings.ToLower(filename[len(filename)-4:]) != ".dot" {
|
||||
filename += ".dot"
|
||||
}
|
||||
|
||||
f, err := os.Create(filename)
|
||||
defer f.Close()
|
||||
if err != nil {
|
||||
t.Errorf("%s", err)
|
||||
} else {
|
||||
memviz.Map(f, cmds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_badGroupings(t *testing.T) {
|
||||
var err error
|
||||
|
||||
// optional groups must be closed
|
||||
_, err = commandline.ParseCommandTemplate([]string{"TEST (arg"})
|
||||
expectFailure(t, err)
|
||||
|
||||
// required groups must be closed
|
||||
_, err = commandline.ParseCommandTemplate([]string{"TEST (arg]"})
|
||||
expectFailure(t, err)
|
||||
}
|
||||
|
||||
func TestParser_goodGroupings(t *testing.T) {
|
||||
var template []string
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
template = []string{"TEST (1 [2] [3] [4] [5])"}
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate(template)
|
||||
if expectSuccess(t, err) {
|
||||
expectEquality(t, template, cmds)
|
||||
}
|
||||
|
||||
template = []string{"TEST [1 [2] [3] [4] [5]]"}
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate(template)
|
||||
if expectSuccess(t, err) {
|
||||
expectEquality(t, template, cmds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_rootGroupings(t *testing.T) {
|
||||
var template []string
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
template = []string{"TEST (arg) %*"}
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate(template)
|
||||
if expectSuccess(t, err) {
|
||||
expectEquality(t, template, cmds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_placeholders(t *testing.T) {
|
||||
var template []string
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
// placeholder directives must be complete
|
||||
_, err = commandline.ParseCommandTemplate([]string{"TEST foo %"})
|
||||
expectFailure(t, err)
|
||||
|
||||
// placeholder directives must be recognised
|
||||
_, err = commandline.ParseCommandTemplate([]string{"TEST foo %q"})
|
||||
expectFailure(t, err)
|
||||
|
||||
// double %% is a valid placeholder directive
|
||||
template = []string{"TEST foo %%"}
|
||||
cmds, err = commandline.ParseCommandTemplate(template)
|
||||
if expectSuccess(t, err) {
|
||||
expectEquality(t, template, cmds)
|
||||
}
|
||||
|
||||
// placeholder directives must be separated from surrounding text
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST foo%%"})
|
||||
expectFailure(t, err)
|
||||
}
|
||||
|
||||
func TestParser_doubleArgs(t *testing.T) {
|
||||
var template []string
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
template = []string{"TEST (egg|fog|nug nog|big) (tug)"}
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate(template)
|
||||
if expectSuccess(t, err) {
|
||||
sort.Stable(cmds)
|
||||
expectEquality(t, template, cmds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
var template []string
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
template = []string{
|
||||
"DISPLAY (OFF|DEBUG|SCALE [%V]|DEBUGCOLORS)",
|
||||
"DROP [BREAK|TRAP|WATCH] [%S]",
|
||||
"GREP %V",
|
||||
"TEST [FOO [%S]|BAR] (EGG [%S]|FOG|NOG NUG) (TUG)",
|
||||
}
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate(template)
|
||||
if expectSuccess(t, err) {
|
||||
expectEquality(t, template, cmds)
|
||||
//memvizOutput(t, "1", cmds)
|
||||
}
|
||||
}
|
210
debugger/commandline/tabcompletion.go
Normal file
210
debugger/commandline/tabcompletion.go
Normal file
|
@ -0,0 +1,210 @@
|
|||
package commandline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const cycleDuration = 500 * time.Millisecond
|
||||
|
||||
// TabCompletion keeps track of the most recent tab completion attempt
|
||||
type TabCompletion struct {
|
||||
commands *Commands
|
||||
|
||||
matches []string
|
||||
match int
|
||||
|
||||
lastCompletion string
|
||||
}
|
||||
|
||||
// NewTabCompletion initialises a new TabCompletion instance
|
||||
//
|
||||
// completion works best if commands has been sorted
|
||||
func NewTabCompletion(commands *Commands) *TabCompletion {
|
||||
tc := new(TabCompletion)
|
||||
tc.commands = commands
|
||||
tc.Reset()
|
||||
return tc
|
||||
}
|
||||
|
||||
// Complete transforms the input such that the last word in the input is
|
||||
// expanded to meet the closest match in the list of allowed strings.
|
||||
func (tc *TabCompletion) Complete(input string) string {
|
||||
// split input tokens -- it's easier to work with tokens
|
||||
tokens := TokeniseInput(input)
|
||||
|
||||
// common function that polishes off a successful Complete()
|
||||
endGuess := func() string {
|
||||
if tc.match >= 0 {
|
||||
tokens.ReplaceEnd(tc.matches[tc.match])
|
||||
tc.lastCompletion = fmt.Sprintf("%s ", tokens.String())
|
||||
} else {
|
||||
// no matches found so completion string is by definition, the same
|
||||
// as the input
|
||||
tc.lastCompletion = input
|
||||
}
|
||||
return tc.lastCompletion
|
||||
}
|
||||
|
||||
// if the input argument is the same as what we returned last time, then
|
||||
// cycle through the options that were compiled last time
|
||||
if tc.lastCompletion == input && tc.match >= 0 {
|
||||
tc.match++
|
||||
if tc.match >= len(tc.matches) {
|
||||
tc.match = 0
|
||||
}
|
||||
return endGuess()
|
||||
}
|
||||
|
||||
// new tabcompletion session
|
||||
|
||||
// reinitialise matches array
|
||||
tc.Reset()
|
||||
|
||||
// no need to to anything if input ends with a space
|
||||
if strings.HasSuffix(input, " ") {
|
||||
return input
|
||||
}
|
||||
|
||||
// get first token
|
||||
tok, ok := tokens.Get()
|
||||
if !ok {
|
||||
return input
|
||||
}
|
||||
tok = strings.ToUpper(tok)
|
||||
|
||||
// look for match
|
||||
for i := range *tc.commands {
|
||||
n := (*tc.commands)[i]
|
||||
|
||||
// if there is an exact match then recurse into the node looking for
|
||||
// where the last token coincides with the node tree
|
||||
if tok == n.tag {
|
||||
// we may have encountered partial matches earlier in the loop. now
|
||||
// that we have found an exact match however, we need to make sure
|
||||
// the match list is empty so that we don't erroneously trigger the
|
||||
// match-cycling branch above.
|
||||
tc.Reset()
|
||||
|
||||
// recurse
|
||||
tokens.Unget()
|
||||
tc.buildMatches(n, tokens)
|
||||
|
||||
return endGuess()
|
||||
}
|
||||
|
||||
// if there is a partial match, then add the current node to the list
|
||||
// of matches
|
||||
if tokens.IsEnd() && len(tok) < len(n.tag) && tok == n.tag[:len(tok)] {
|
||||
tc.matches = append(tc.matches, n.tag)
|
||||
tc.match = 0
|
||||
}
|
||||
}
|
||||
|
||||
return endGuess()
|
||||
}
|
||||
|
||||
// Reset is used to clear an outstanding completion session. note that this
|
||||
// only really needs to be called if the input argument to Complete() is not
|
||||
// different to the previous return value from that function, and you want to
|
||||
// start a new completion session.
|
||||
func (tc *TabCompletion) Reset() {
|
||||
tc.matches = make([]string, 0)
|
||||
tc.match = -1
|
||||
}
|
||||
|
||||
func (tc *TabCompletion) buildMatches(n *node, tokens *Tokens) {
|
||||
// if there is no more input then return true (validation has passed) if
|
||||
// the node is optional, false if it is required
|
||||
tok, ok := tokens.Get()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
match := true
|
||||
switch n.tag {
|
||||
case "%V":
|
||||
_, err := strconv.ParseInt(tok, 0, 32)
|
||||
match = err == nil
|
||||
|
||||
case "%I":
|
||||
_, err := strconv.ParseFloat(tok, 32)
|
||||
match = err == nil
|
||||
|
||||
case "%S":
|
||||
// accept anything
|
||||
|
||||
case "%F":
|
||||
// TODO: filename completion
|
||||
|
||||
case "%*":
|
||||
// this placeholder indicates that the rest of the tokens can be
|
||||
// ignored.
|
||||
return
|
||||
|
||||
default:
|
||||
// case sensitive matching
|
||||
tok = strings.ToUpper(tok)
|
||||
match = tok == n.tag
|
||||
}
|
||||
|
||||
// if token doesn't match this node, check branches. if there are no
|
||||
// branches, return false (validation has failed)
|
||||
if !match {
|
||||
// if there is a partial match, then add the current node to the list
|
||||
// of matches
|
||||
if tokens.IsEnd() && len(tok) < len(n.tag) && tok == n.tag[:len(tok)] {
|
||||
tc.matches = append(tc.matches, n.tag)
|
||||
tc.match = 0
|
||||
}
|
||||
|
||||
if n.branch == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// take a note of current token position. if the token wanders past
|
||||
// this point as a result of a branch then we can see that the branch
|
||||
// was deeper then just one token. if this is the case then we can see
|
||||
// that the branch was *partially* accepted and that we should not
|
||||
// proceed onto next-nodes from here.
|
||||
tokenAt := tokens.curr
|
||||
|
||||
for bi := range n.branch {
|
||||
// we want to use the current token again so we unget() the last token
|
||||
// so that it is available at the beginning of the recursed validate()
|
||||
// function
|
||||
tokens.Unget()
|
||||
|
||||
tc.buildMatches(n.branch[bi], tokens)
|
||||
}
|
||||
|
||||
// the key to this condition is the tokenAt variable. see note above.
|
||||
if n.group == groupOptional && len(tc.matches) == 0 && tokenAt == tokens.curr {
|
||||
tokens.Unget()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// token does match and there are no more tokens to consume so we can add
|
||||
// this successful token to the list of matches
|
||||
//
|
||||
// note that this is specific to tab-completion, validation has no
|
||||
// equivalent. the purpose of this is to cause the Complete() function
|
||||
// above to replace the last token with a normalised version of that token
|
||||
// and to suffix it with a space.
|
||||
if tokens.IsEnd() {
|
||||
tc.matches = append(tc.matches, tok)
|
||||
tc.match = 0
|
||||
return
|
||||
}
|
||||
|
||||
// token does match this node. check nodes that follow on.
|
||||
for nx := range n.next {
|
||||
tc.buildMatches(n.next[nx], tokens)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
192
debugger/commandline/tabcompletion_test.go
Normal file
192
debugger/commandline/tabcompletion_test.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
package commandline_test
|
||||
|
||||
import (
|
||||
"gopher2600/debugger/commandline"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTabCompletion(t *testing.T) {
|
||||
var cmds *commandline.Commands
|
||||
var tc *commandline.TabCompletion
|
||||
var completion, expected string
|
||||
var err error
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{
|
||||
"TEST [arg]",
|
||||
"TEST1 [arg]",
|
||||
"FOO [bar|baz] wibble",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
sort.Stable(cmds)
|
||||
|
||||
tc = commandline.NewTabCompletion(cmds)
|
||||
|
||||
completion = "TE"
|
||||
expected = "TEST "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
// next completion option
|
||||
expected = "TEST1 "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
// cycle back to the first completion option
|
||||
expected = "TEST "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
tc.Reset()
|
||||
completion = "TEST a"
|
||||
expected = "TEST ARG "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
tc.Reset()
|
||||
completion = "FOO ba"
|
||||
expected = "FOO BAR "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
expected = "FOO BAZ "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
// the completer will preserve whitespace
|
||||
tc.Reset()
|
||||
completion = "FOO bar wib"
|
||||
expected = "FOO bar WIBBLE "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTabCompletion_placeholders(t *testing.T) {
|
||||
var cmds *commandline.Commands
|
||||
var tc *commandline.TabCompletion
|
||||
var completion, expected string
|
||||
var err error
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{
|
||||
"TEST %V (foo|bar)",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
sort.Stable(cmds)
|
||||
|
||||
tc = commandline.NewTabCompletion(cmds)
|
||||
|
||||
completion = "TEST 100 f"
|
||||
expected = "TEST 100 FOO "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTabCompletion_doubleArgs(t *testing.T) {
|
||||
var cmds *commandline.Commands
|
||||
var tc *commandline.TabCompletion
|
||||
var completion, expected string
|
||||
var err error
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (egg|fog|nug nog|big) (tug)"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
tc = commandline.NewTabCompletion(cmds)
|
||||
|
||||
completion = "TEST eg"
|
||||
expected = "TEST EGG "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
completion = "TEST egg T"
|
||||
expected = "TEST egg TUG "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
completion = "TEST n"
|
||||
expected = "TEST NUG "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
completion = "TEST nug N"
|
||||
expected = "TEST nug NOG "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
completion = "TEST T"
|
||||
expected = "TEST TUG "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
completion = "TEST nug nog T"
|
||||
expected = "TEST nug nog TUG "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTabCompletion_complex(t *testing.T) {
|
||||
var cmds *commandline.Commands
|
||||
var tc *commandline.TabCompletion
|
||||
var completion, expected string
|
||||
var err error
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg [%V|bar]|foo) %*"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
tc = commandline.NewTabCompletion(cmds)
|
||||
|
||||
completion = "TEST ar"
|
||||
expected = "TEST ARG "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
completion = "TEST arg b"
|
||||
expected = "TEST arg BAR "
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
|
||||
completion = "TEST arg 10 wib"
|
||||
expected = "TEST arg 10 wib"
|
||||
completion = tc.Complete(completion)
|
||||
if completion != expected {
|
||||
t.Errorf("expecting '%s' got '%s'", expected, completion)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package input
|
||||
package commandline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -8,17 +8,20 @@ import (
|
|||
// Tokens represents tokenised input. This can be used to walk through the
|
||||
// input string (using get()) for eas(ier) parsing
|
||||
type Tokens struct {
|
||||
input string
|
||||
tokens []string
|
||||
curr int
|
||||
}
|
||||
|
||||
func (tk *Tokens) String() string {
|
||||
s := strings.Builder{}
|
||||
for i := range tk.tokens {
|
||||
s.WriteString(tk.tokens[i])
|
||||
s.WriteString(" ")
|
||||
}
|
||||
return s.String()
|
||||
return tk.input
|
||||
|
||||
// s := strings.Builder{}
|
||||
// for i := range tk.tokens {
|
||||
// s.WriteString(tk.tokens[i])
|
||||
// s.WriteString(" ")
|
||||
// }
|
||||
// return s.String()
|
||||
}
|
||||
|
||||
// Reset begins the token traversal process from the beginning
|
||||
|
@ -26,6 +29,11 @@ func (tk *Tokens) Reset() {
|
|||
tk.curr = 0
|
||||
}
|
||||
|
||||
// IsEnd returns true if we're at the end of the token list
|
||||
func (tk Tokens) IsEnd() bool {
|
||||
return tk.curr >= len(tk.tokens)
|
||||
}
|
||||
|
||||
// Remainder returns the remaining tokens as a string
|
||||
func (tk Tokens) Remainder() string {
|
||||
return strings.Join(tk.tokens[tk.curr:], " ")
|
||||
|
@ -36,9 +44,14 @@ func (tk Tokens) Remaining() int {
|
|||
return len(tk.tokens) - tk.curr
|
||||
}
|
||||
|
||||
// Total returns the total count of tokens
|
||||
func (tk Tokens) Total() int {
|
||||
return len(tk.tokens)
|
||||
// ReplaceEnd changes the last entry of the token list
|
||||
func (tk *Tokens) ReplaceEnd(newEnd string) {
|
||||
// change end of original string
|
||||
t := strings.LastIndex(tk.input, tk.tokens[len(tk.tokens)-1])
|
||||
tk.input = tk.input[:t] + newEnd
|
||||
|
||||
// change tokens
|
||||
tk.tokens[len(tk.tokens)-1] = newEnd
|
||||
}
|
||||
|
||||
// Get returns the next token in the list, and a success boolean - if the end
|
||||
|
@ -76,9 +89,12 @@ func TokeniseInput(input string) *Tokens {
|
|||
// remove leading/trailing space
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
// divide user input into tokens
|
||||
// divide user input into tokens. removes excess white space
|
||||
tk.tokens = tokeniseInput(input)
|
||||
|
||||
// take a note of the raw input
|
||||
tk.input = input
|
||||
|
||||
// normalise variations in syntax
|
||||
for i := 0; i < len(tk.tokens); i++ {
|
||||
// normalise hex notation
|
||||
|
@ -93,7 +109,7 @@ func TokeniseInput(input string) *Tokens {
|
|||
// tokeniseInput is the "raw" tokenising function (without normalisation or
|
||||
// wrapping everything up in a Tokens instance). used by the fancier
|
||||
// TokeniseInput and anywhere else where we need to divide input into tokens
|
||||
// (eg. TabCompletion.GuessWord())
|
||||
// (eg. TabCompletion.Complete())
|
||||
func tokeniseInput(input string) []string {
|
||||
return strings.Fields(input)
|
||||
}
|
173
debugger/commandline/validation.go
Normal file
173
debugger/commandline/validation.go
Normal file
|
@ -0,0 +1,173 @@
|
|||
package commandline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Validate input string against command defintions
|
||||
func (cmds Commands) Validate(input string) error {
|
||||
return cmds.ValidateTokens(TokeniseInput(input))
|
||||
}
|
||||
|
||||
// ValidateTokens like Validate, but works on tokens rather than an input
|
||||
// string
|
||||
func (cmds Commands) ValidateTokens(tokens *Tokens) error {
|
||||
cmd, ok := tokens.Peek()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
cmd = strings.ToUpper(cmd)
|
||||
|
||||
for n := range cmds {
|
||||
if cmd == cmds[n].tag {
|
||||
|
||||
err := cmds[n].validate(tokens)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s for %s", err, cmd)
|
||||
}
|
||||
|
||||
if tokens.Remaining() > 0 {
|
||||
return fmt.Errorf("too many arguments for %s", cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unrecognised command (%s)", cmd)
|
||||
}
|
||||
|
||||
// branches creates a readable string, listing all the branches of the node
|
||||
func branches(n *node) string {
|
||||
s := strings.Builder{}
|
||||
s.WriteString(n.tag)
|
||||
for bi := range n.branch {
|
||||
s.WriteString(", ")
|
||||
s.WriteString(n.branch[bi].tag)
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (n *node) validate(tokens *Tokens) error {
|
||||
// if there is no more input then return true (validation has passed) if
|
||||
// the node is optional, false if it is required
|
||||
tok, ok := tokens.Get()
|
||||
if !ok {
|
||||
// we treat arguments in the root-group as though they are required,
|
||||
// with the exception of the %* placeholder
|
||||
if n.group == groupRequired || (n.group == groupRoot && n.tag != "%*") {
|
||||
// replace placeholder arguments with something a little less cryptic
|
||||
switch n.tag {
|
||||
case "%*":
|
||||
return fmt.Errorf("missing required arguments")
|
||||
case "%S":
|
||||
return fmt.Errorf("missing string argument")
|
||||
case "%V":
|
||||
return fmt.Errorf("missing numeric argument")
|
||||
case "%I":
|
||||
return fmt.Errorf("missing floating-point argument")
|
||||
case "%F":
|
||||
return fmt.Errorf("missing filename argument")
|
||||
}
|
||||
return fmt.Errorf("missing a required argument (%s)", branches(n))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// check to see if input matches this node. using placeholder matching if
|
||||
// appropriate
|
||||
|
||||
match := true
|
||||
|
||||
// default error in case nothing matches - replaced as necessary
|
||||
err := fmt.Errorf("unrecognised argument (%s)", tok)
|
||||
|
||||
switch n.tag {
|
||||
case "%V":
|
||||
_, err := strconv.ParseInt(tok, 0, 32)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("numeric argument required (%s is not numeric)", tok)
|
||||
match = false
|
||||
}
|
||||
|
||||
case "%I":
|
||||
_, err := strconv.ParseFloat(tok, 32)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("float argument required (%s is not numeric)", tok)
|
||||
match = false
|
||||
}
|
||||
|
||||
case "%S":
|
||||
// accept anything
|
||||
|
||||
case "%F":
|
||||
// accept anything (note: filename is distinct from %S when we use it
|
||||
// for tab-completion)
|
||||
|
||||
case "%*":
|
||||
// this placeholder indicates that the rest of the tokens can be
|
||||
// ignored.
|
||||
|
||||
// consume the rest of the tokens without a care
|
||||
for ok {
|
||||
_, ok = tokens.Get()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
default:
|
||||
// case sensitive matching
|
||||
tok = strings.ToUpper(tok)
|
||||
match = tok == n.tag
|
||||
}
|
||||
|
||||
// if input doesn't match this node, check branches
|
||||
if !match {
|
||||
if n.branch != nil {
|
||||
for bi := range n.branch {
|
||||
// recursing into the validate function means we need to use the
|
||||
// same token as above. Unget() prepares the tokens object for
|
||||
// that.
|
||||
tokens.Unget()
|
||||
|
||||
if n.branch[bi].validate(tokens) == nil {
|
||||
|
||||
// break loop on first successful branch
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// tricky condition: if we've not found anything in any of the
|
||||
// branches and this is an optional group, then claim that we have
|
||||
// matched this group and prepare tokens object for additional
|
||||
// nodes. if group is not optional then return error.
|
||||
if !match {
|
||||
if n.group == groupOptional {
|
||||
tokens.Unget()
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if !match {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// input does match this node. check nodes that follow on.
|
||||
for ni := range n.next {
|
||||
err := n.next[ni].validate(tokens)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
230
debugger/commandline/validation_test.go
Normal file
230
debugger/commandline/validation_test.go
Normal file
|
@ -0,0 +1,230 @@
|
|||
package commandline_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopher2600/debugger/commandline"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidation_required(t *testing.T) {
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST [arg]"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST arg foo")
|
||||
if err == nil {
|
||||
t.Errorf("matches but shouldn't")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST arg")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST")
|
||||
if err == nil {
|
||||
t.Errorf("matches but shouldn't")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidation_optional(t *testing.T) {
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg)"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST arg")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST arg foo")
|
||||
if err == nil {
|
||||
t.Errorf("matches but shouldn't")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST foo")
|
||||
if err == nil {
|
||||
t.Errorf("matches but shouldn't")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidation_branchesAndNumeric(t *testing.T) {
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg [%V]|foo) %*"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST foo wibble")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST arg")
|
||||
if err == nil {
|
||||
t.Errorf("matches but shouldn't")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
// numeric argument matching
|
||||
err = cmds.Validate("TEST arg 10")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
// failing a numeric argument match
|
||||
err = cmds.Validate("TEST arg bar")
|
||||
if err == nil {
|
||||
t.Errorf("matches but shouldn't")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
// ---------------
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg|foo) %V"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST arg")
|
||||
if err == nil {
|
||||
t.Errorf("matches but shouldn't")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST arg 10")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST 10")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidation_deepBranches(t *testing.T) {
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
// retry numeric argument matching but with an option for a specific string
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg [%V|bar]|foo) %*"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST arg bar")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST arg foo")
|
||||
if err == nil {
|
||||
t.Errorf("matches but shouldn't")
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidation_tripleBranches(t *testing.T) {
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (arg|foo|bar) wibble"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST foo wibble")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST bar wibble")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST wibble")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidation_doubleArgs(t *testing.T) {
|
||||
var cmds *commandline.Commands
|
||||
var err error
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (nug nog|egg|cream) (tug)"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST nug nog")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST egg tug")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST nug nog tug")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
// ---------------
|
||||
|
||||
cmds, err = commandline.ParseCommandTemplate([]string{"TEST (egg|fog|nug nog|big) (tug)"})
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST nug nog")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST fog tug")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
|
||||
err = cmds.Validate("TEST nug nog tug")
|
||||
if err != nil {
|
||||
t.Errorf("doesn't match but should: %s", err)
|
||||
}
|
||||
}
|
|
@ -2,14 +2,15 @@ package debugger
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"gopher2600/debugger/commandline"
|
||||
"gopher2600/debugger/console"
|
||||
"gopher2600/debugger/input"
|
||||
"gopher2600/errors"
|
||||
"gopher2600/gui"
|
||||
"gopher2600/hardware/cpu/result"
|
||||
"gopher2600/symbols"
|
||||
"gopher2600/television"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
@ -49,8 +50,7 @@ const (
|
|||
cmdScript = "SCRIPT"
|
||||
cmdStep = "STEP"
|
||||
cmdStepMode = "STEPMODE"
|
||||
cmdStick0 = "STICK0"
|
||||
cmdStick1 = "STICK1"
|
||||
cmdStick = "STICK"
|
||||
cmdSymbol = "SYMBOL"
|
||||
cmdTIA = "TIA"
|
||||
cmdTV = "TV"
|
||||
|
@ -61,70 +61,62 @@ const (
|
|||
cmdWatch = "WATCH"
|
||||
)
|
||||
|
||||
// notes
|
||||
// o KeywordStep can take a valid target
|
||||
// o KeywordDisplay SCALE takes an additional argument but OFF and DEBUG do
|
||||
// not. the %* is a compromise
|
||||
|
||||
// break/trap/watch values are parsed in parseTargets() function
|
||||
// TODO: find some way to create valid templates using information from
|
||||
// other sources
|
||||
|
||||
var commandTemplate = input.CommandTemplate{
|
||||
cmdBall: "",
|
||||
cmdBreak: "%*",
|
||||
cmdCPU: "",
|
||||
cmdCapture: "[END|%F]",
|
||||
cmdCartridge: "",
|
||||
cmdClear: "[BREAKS|TRAPS|WATCHES]",
|
||||
cmdDebuggerState: "",
|
||||
cmdDisassemble: "",
|
||||
cmdDisplay: "[|OFF|DEBUG|SCALE|DEBUGCOLORS] %*", // see notes
|
||||
cmdDrop: "[BREAK|TRAP|WATCH] %V",
|
||||
cmdGrep: "%S %*",
|
||||
cmdHexLoad: "%*",
|
||||
cmdInsert: "%F",
|
||||
cmdLast: "[|DEFN]",
|
||||
cmdList: "[BREAKS|TRAPS|WATCHES]",
|
||||
cmdMemMap: "",
|
||||
cmdMissile: "",
|
||||
cmdOnHalt: "[|OFF|RESTORE] %*",
|
||||
cmdOnStep: "[|OFF|RESTORE] %*",
|
||||
cmdPeek: "%*",
|
||||
cmdPlayer: "",
|
||||
cmdPlayfield: "",
|
||||
cmdPoke: "%*",
|
||||
cmdQuit: "",
|
||||
cmdRAM: "",
|
||||
cmdRIOT: "",
|
||||
cmdReset: "",
|
||||
cmdRun: "",
|
||||
cmdScript: "%F",
|
||||
cmdStep: "[|CPU|VIDEO|SCANLINE]", // see notes
|
||||
cmdStepMode: "[|CPU|VIDEO]",
|
||||
cmdStick0: "[LEFT|RIGHT|UP|DOWN|FIRE|CENTRE|NOFIRE]",
|
||||
cmdStick1: "[LEFT|RIGHT|UP|DOWN|FIRE|CENTRE|NOFIRE]",
|
||||
cmdSymbol: "%S [|ALL]",
|
||||
cmdTIA: "[|FUTURE|HMOVE]",
|
||||
cmdTV: "[|SPEC]",
|
||||
cmdTerse: "",
|
||||
cmdTrap: "%*",
|
||||
cmdVerbose: "",
|
||||
cmdVerbosity: "",
|
||||
cmdWatch: "[READ|WRITE|] %V %*",
|
||||
var expCommandTemplate = []string{
|
||||
cmdBall,
|
||||
cmdBreak + " [%*]",
|
||||
cmdCPU,
|
||||
cmdCapture + " [END|%F]",
|
||||
cmdCartridge,
|
||||
cmdClear + " [BREAKS|TRAPS|WATCHES]",
|
||||
cmdDebuggerState,
|
||||
cmdDisassemble,
|
||||
cmdDisplay + " (OFF|DEBUG|SCALE [%I]|DEBUGCOLORS)", // see notes
|
||||
cmdDrop + " [BREAK|TRAP|WATCH] %V",
|
||||
cmdGrep + " %V",
|
||||
cmdHelp + " %*",
|
||||
cmdHexLoad + " %V %*",
|
||||
cmdInsert + " %F",
|
||||
cmdLast + " (DEFN)",
|
||||
cmdList + " [BREAKS|TRAPS|WATCHES]",
|
||||
cmdMemMap,
|
||||
cmdMissile + "(0|1)",
|
||||
cmdOnHalt + " (OFF|RESTORE|%*)",
|
||||
cmdOnStep + " (OFF|RESTORE|%*)",
|
||||
cmdPeek + " %V %*",
|
||||
cmdPlayer + "(0|1)",
|
||||
cmdPlayfield,
|
||||
cmdPoke + " %V %*",
|
||||
cmdQuit,
|
||||
cmdRAM,
|
||||
cmdRIOT,
|
||||
cmdReset,
|
||||
cmdRun,
|
||||
cmdScript + " %F",
|
||||
cmdStep + " (CPU|VIDEO|SCANLINE)", // see notes
|
||||
cmdStepMode + " (CPU|VIDEO)",
|
||||
cmdStick + "[0|1] [LEFT|RIGHT|UP|DOWN|FIRE|CENTRE|NOFIRE]",
|
||||
cmdSymbol + " %V (ALL)",
|
||||
cmdTIA + " (FUTURE|HMOVE)",
|
||||
cmdTV + " (SPEC)",
|
||||
cmdTerse,
|
||||
cmdTrap + " [%*]",
|
||||
cmdVerbose,
|
||||
cmdVerbosity,
|
||||
cmdWatch + " (READ|WRITE) [%V]",
|
||||
}
|
||||
|
||||
// DebuggerCommands is the tree of valid commands
|
||||
var DebuggerCommands input.Commands
|
||||
var debuggerCommands *commandline.Commands
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
// parse command template
|
||||
DebuggerCommands, err = input.CompileCommandTemplate(commandTemplate, cmdHelp)
|
||||
debuggerCommands, err = commandline.ParseCommandTemplate(expCommandTemplate)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("error compiling command template: %s", err))
|
||||
fmt.Println(err)
|
||||
os.Exit(100)
|
||||
}
|
||||
sort.Stable(debuggerCommands)
|
||||
}
|
||||
|
||||
type parseCommandResult int
|
||||
|
@ -146,25 +138,41 @@ const (
|
|||
// TODO: categorise commands into script-safe and non-script-safe
|
||||
func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error) {
|
||||
// tokenise input
|
||||
tokens := input.TokeniseInput(*userInput)
|
||||
tokens := commandline.TokeniseInput(*userInput)
|
||||
|
||||
// check validity of input
|
||||
err := DebuggerCommands.ValidateInput(tokens)
|
||||
if err != nil {
|
||||
return doNothing, err
|
||||
}
|
||||
// normalise user input -- we don't use the results in this
|
||||
// function but we do use it futher-up. eg. when capturing user input to a
|
||||
// script
|
||||
*userInput = tokens.String()
|
||||
|
||||
// if there are no tokens in the input then return emptyInput directive
|
||||
if tokens.Remaining() == 0 {
|
||||
return emptyInput, nil
|
||||
}
|
||||
|
||||
// normalise user input
|
||||
*userInput = tokens.String()
|
||||
// check validity of tokenised input
|
||||
//
|
||||
// the absolute best thing about this is that we don't need to worrying too
|
||||
// much about the success of tokens.Get() in the command implementations
|
||||
// below:
|
||||
//
|
||||
// tok, _ := tokens.Get()
|
||||
//
|
||||
// is an acceptable pattern
|
||||
err := debuggerCommands.ValidateTokens(tokens)
|
||||
if err != nil {
|
||||
return doNothing, err
|
||||
}
|
||||
|
||||
// check first token. if this token makes sense then we will consume the
|
||||
// rest of the tokens appropriately
|
||||
tokens.Reset()
|
||||
command, _ := tokens.Get()
|
||||
|
||||
// take uppercase value of the first token. it's useful to take the
|
||||
// uppercase value but we have to be careful when we do it because
|
||||
command = strings.ToUpper(command)
|
||||
|
||||
switch command {
|
||||
default:
|
||||
return doNothing, fmt.Errorf("%s is not yet implemented", command)
|
||||
|
@ -180,9 +188,7 @@ func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error)
|
|||
dbg.print(console.Help, txt)
|
||||
}
|
||||
} else {
|
||||
for k := range DebuggerCommands {
|
||||
dbg.print(console.Help, k)
|
||||
}
|
||||
dbg.print(console.Help, debuggerCommands.String())
|
||||
}
|
||||
|
||||
case cmdInsert:
|
||||
|
@ -211,7 +217,7 @@ func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error)
|
|||
dbg.disasm.Dump(os.Stdout)
|
||||
|
||||
case cmdGrep:
|
||||
search := tokens.Remainder()
|
||||
search, _ := tokens.Get()
|
||||
output := strings.Builder{}
|
||||
dbg.disasm.Grep(search, &output, false, 3)
|
||||
if output.Len() == 0 {
|
||||
|
@ -509,7 +515,7 @@ func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error)
|
|||
}
|
||||
|
||||
case cmdDebuggerState:
|
||||
_, err := dbg.parseInput("VERBOSITY; STEPMODE; ONHALT ECHO; ONSTEP ECHO", false)
|
||||
_, err := dbg.parseInput("VERBOSITY; STEPMODE; ONHALT; ONSTEP", false)
|
||||
if err != nil {
|
||||
return doNothing, err
|
||||
}
|
||||
|
@ -670,61 +676,121 @@ func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error)
|
|||
|
||||
// information about the machine (sprites, playfield)
|
||||
case cmdPlayer:
|
||||
// TODO: argument to print either player 0 or player 1
|
||||
plyr := -1
|
||||
|
||||
tok, _ := tokens.Get()
|
||||
switch tok {
|
||||
case "0":
|
||||
plyr = 0
|
||||
case "1":
|
||||
plyr = 1
|
||||
default:
|
||||
tokens.Unget()
|
||||
}
|
||||
|
||||
if dbg.machineInfoVerbose {
|
||||
// arrange the two player's information side by side in order to
|
||||
// save space and to allow for easy comparison
|
||||
|
||||
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player0), "\n")
|
||||
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player1), "\n")
|
||||
switch plyr {
|
||||
case 0:
|
||||
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player0), "\n")
|
||||
dbg.print(console.MachineInfo, strings.Join(p0, "\n"))
|
||||
|
||||
ml := 0
|
||||
for i := range p0 {
|
||||
if len(p0[i]) > ml {
|
||||
ml = len(p0[i])
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player1), "\n")
|
||||
dbg.print(console.MachineInfo, strings.Join(p1, "\n"))
|
||||
|
||||
s := strings.Builder{}
|
||||
for i := range p0 {
|
||||
if p0[i] != "" {
|
||||
s.WriteString(fmt.Sprintf("%s %s | %s\n", p0[i], strings.Repeat(" ", ml-len(p0[i])), p1[i]))
|
||||
default:
|
||||
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player0), "\n")
|
||||
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Player1), "\n")
|
||||
|
||||
ml := 0
|
||||
for i := range p0 {
|
||||
if len(p0[i]) > ml {
|
||||
ml = len(p0[i])
|
||||
}
|
||||
}
|
||||
|
||||
s := strings.Builder{}
|
||||
for i := range p0 {
|
||||
if p0[i] != "" {
|
||||
s.WriteString(fmt.Sprintf("%s %s | %s\n", p0[i], strings.Repeat(" ", ml-len(p0[i])), p1[i]))
|
||||
}
|
||||
}
|
||||
dbg.print(console.MachineInfo, s.String())
|
||||
}
|
||||
dbg.print(console.MachineInfo, s.String())
|
||||
} else {
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player0)
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player1)
|
||||
switch plyr {
|
||||
case 0:
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player0)
|
||||
|
||||
case 1:
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player1)
|
||||
|
||||
default:
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player0)
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Player1)
|
||||
}
|
||||
}
|
||||
|
||||
case cmdMissile:
|
||||
// TODO: argument to print either missile 0 or missile 1
|
||||
mssl := -1
|
||||
|
||||
tok, _ := tokens.Get()
|
||||
switch tok {
|
||||
case "0":
|
||||
mssl = 0
|
||||
case "1":
|
||||
mssl = 1
|
||||
default:
|
||||
tokens.Unget()
|
||||
}
|
||||
|
||||
if dbg.machineInfoVerbose {
|
||||
// arrange the two missile's information side by side in order to
|
||||
// save space and to allow for easy comparison
|
||||
switch mssl {
|
||||
case 0:
|
||||
m0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile0), "\n")
|
||||
dbg.print(console.MachineInfo, strings.Join(m0, "\n"))
|
||||
|
||||
p0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile0), "\n")
|
||||
p1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile1), "\n")
|
||||
case 1:
|
||||
m1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile0), "\n")
|
||||
dbg.print(console.MachineInfo, strings.Join(m1, "\n"))
|
||||
|
||||
ml := 0
|
||||
for i := range p0 {
|
||||
if len(p0[i]) > ml {
|
||||
ml = len(p0[i])
|
||||
default:
|
||||
// arrange the two missile's information side by side in order to
|
||||
// save space and to allow for easy comparison
|
||||
|
||||
m0 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile0), "\n")
|
||||
m1 := strings.Split(dbg.getMachineInfo(dbg.vcs.TIA.Video.Missile1), "\n")
|
||||
|
||||
ml := 0
|
||||
for i := range m0 {
|
||||
if len(m0[i]) > ml {
|
||||
ml = len(m0[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s := strings.Builder{}
|
||||
for i := range p0 {
|
||||
if p0[i] != "" {
|
||||
s.WriteString(fmt.Sprintf("%s %s | %s\n", p0[i], strings.Repeat(" ", ml-len(p0[i])), p1[i]))
|
||||
s := strings.Builder{}
|
||||
for i := range m0 {
|
||||
if m0[i] != "" {
|
||||
s.WriteString(fmt.Sprintf("%s %s | %s\n", m0[i], strings.Repeat(" ", ml-len(m0[i])), m1[i]))
|
||||
}
|
||||
}
|
||||
dbg.print(console.MachineInfo, s.String())
|
||||
}
|
||||
dbg.print(console.MachineInfo, s.String())
|
||||
} else {
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile0)
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile1)
|
||||
switch mssl {
|
||||
case 0:
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile0)
|
||||
|
||||
case 1:
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile1)
|
||||
|
||||
default:
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile0)
|
||||
dbg.printMachineInfo(dbg.vcs.TIA.Video.Missile1)
|
||||
}
|
||||
}
|
||||
|
||||
case cmdBall:
|
||||
|
@ -783,22 +849,15 @@ func (dbg *Debugger) parseCommand(userInput *string) (parseCommandResult, error)
|
|||
}
|
||||
}
|
||||
|
||||
case cmdStick0:
|
||||
action, present := tokens.Get()
|
||||
if present {
|
||||
err := dbg.vcs.Controller.HandleStick(0, action)
|
||||
if err != nil {
|
||||
return doNothing, err
|
||||
}
|
||||
}
|
||||
case cmdStick:
|
||||
stick, _ := tokens.Get()
|
||||
action, _ := tokens.Get()
|
||||
|
||||
case cmdStick1:
|
||||
action, present := tokens.Get()
|
||||
if present {
|
||||
err := dbg.vcs.Controller.HandleStick(1, action)
|
||||
if err != nil {
|
||||
return doNothing, err
|
||||
}
|
||||
stickN, _ := strconv.Atoi(stick)
|
||||
|
||||
err := dbg.vcs.Controller.HandleStick(stickN, action)
|
||||
if err != nil {
|
||||
return doNothing, err
|
||||
}
|
||||
|
||||
case cmdCapture:
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package console
|
||||
|
||||
import (
|
||||
"gopher2600/debugger/input"
|
||||
"gopher2600/gui"
|
||||
)
|
||||
|
||||
|
@ -21,7 +20,13 @@ type UserOutput interface {
|
|||
type UserInterface interface {
|
||||
Initialise() error
|
||||
CleanUp()
|
||||
RegisterTabCompleter(*input.TabCompletion)
|
||||
RegisterTabCompleter(TabCompleter)
|
||||
UserInput
|
||||
UserOutput
|
||||
}
|
||||
|
||||
// TabCompleter defines the operations required for tab completion
|
||||
type TabCompleter interface {
|
||||
Complete(input string) string
|
||||
Reset()
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package console
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"gopher2600/debugger/input"
|
||||
"gopher2600/gui"
|
||||
"os"
|
||||
)
|
||||
|
@ -21,7 +20,7 @@ func (pt *PlainTerminal) CleanUp() {
|
|||
}
|
||||
|
||||
// RegisterTabCompleter adds an implementation of TabCompleter to the terminal
|
||||
func (pt *PlainTerminal) RegisterTabCompleter(tc *input.TabCompletion) {
|
||||
func (pt *PlainTerminal) RegisterTabCompleter(TabCompleter) {
|
||||
}
|
||||
|
||||
// UserPrint is the plain terminal print routine
|
||||
|
|
|
@ -2,8 +2,8 @@ package debugger
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"gopher2600/debugger/commandline"
|
||||
"gopher2600/debugger/console"
|
||||
"gopher2600/debugger/input"
|
||||
"gopher2600/debugger/monitor"
|
||||
"gopher2600/disassembly"
|
||||
"gopher2600/errors"
|
||||
|
@ -184,7 +184,7 @@ func (dbg *Debugger) Start(iface console.UserInterface, filename string, initScr
|
|||
}
|
||||
defer dbg.console.CleanUp()
|
||||
|
||||
dbg.console.RegisterTabCompleter(input.NewTabCompletion(DebuggerCommands))
|
||||
dbg.console.RegisterTabCompleter(commandline.NewTabCompletion(debuggerCommands))
|
||||
|
||||
err = dbg.loadCartridge(filename)
|
||||
if err != nil {
|
||||
|
|
|
@ -2,7 +2,7 @@ package debugger
|
|||
|
||||
// Help contains the help text for the debugger's top level commands
|
||||
var Help = map[string]string{
|
||||
cmdBall: "Display the current state of the Ball sprite",
|
||||
cmdBall: "Display the current state of the ball sprite",
|
||||
cmdBreak: "Cause emulator to halt when conditions are met",
|
||||
cmdCPU: "Display the current state of the CPU",
|
||||
cmdCapture: "Start capturing entered commands to an extermnal script",
|
||||
|
@ -19,11 +19,11 @@ var Help = map[string]string{
|
|||
cmdLast: "Prints the result of the last cpu/video cycle",
|
||||
cmdList: "List current entries for BREAKS and TRAPS",
|
||||
cmdMemMap: "Display high-level VCS memory map",
|
||||
cmdMissile: "Display the current state of the Missile 0/1 sprite",
|
||||
cmdMissile: "Display the current state of the missile 0/1 sprite",
|
||||
cmdOnHalt: "Commands to run whenever emulation is halted (separate commands with comma)",
|
||||
cmdOnStep: "Commands to run whenever emulation steps forward an cpu/video cycle (separate commands with comma)",
|
||||
cmdPeek: "Inspect an individual memory address",
|
||||
cmdPlayer: "Display the current state of the Player 0/1 sprite",
|
||||
cmdPlayer: "Display the current state of the player 0/1 sprite",
|
||||
cmdPlayfield: "Display the current playfield data",
|
||||
cmdPoke: "Modify an individual memory address",
|
||||
cmdQuit: "Exits the emulator",
|
||||
|
@ -34,8 +34,7 @@ var Help = map[string]string{
|
|||
cmdScript: "Run commands from specified file",
|
||||
cmdStep: "Step forward emulator one step (see STEPMODE command)",
|
||||
cmdStepMode: "Change method of stepping: CPU or VIDEO",
|
||||
cmdStick0: "Emulate a joystick input for Player 0",
|
||||
cmdStick1: "Emulate a joystick input for Player 1",
|
||||
cmdStick: "Emulate a joystick input for Player 0 or Player 1",
|
||||
cmdSymbol: "Search for the address label symbol in disassembly. returns address",
|
||||
cmdTIA: "Display current state of the TIA",
|
||||
cmdTV: "Display the current TV state",
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
package input
|
||||
|
||||
// Commands is the root of the argument "tree"
|
||||
type Commands map[string]commandArgList
|
||||
|
||||
// commandArgList is the list of commandArgList for each command
|
||||
type commandArgList []commandArg
|
||||
|
||||
// maximumLen returns the maximum number of arguments allowed for a given
|
||||
// command
|
||||
func (a commandArgList) maximumLen() int {
|
||||
if len(a) == 0 {
|
||||
return 0
|
||||
}
|
||||
if a[len(a)-1].typ == argIndeterminate {
|
||||
// to indicate indeterminancy, return the maximum value allowed for an integer
|
||||
return int(^uint(0) >> 1)
|
||||
}
|
||||
return len(a)
|
||||
}
|
||||
|
||||
// requiredLen returns the number of arguments required for a given command.
|
||||
// in other words, the command may allow more but it must have at least the
|
||||
// returned numnber.
|
||||
func (a commandArgList) requiredLen() (m int) {
|
||||
for i := 0; i < len(a); i++ {
|
||||
if !a[i].required {
|
||||
return
|
||||
}
|
||||
m++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// argType defines the expected argument type
|
||||
type argType int
|
||||
|
||||
// the possible values for argType
|
||||
const (
|
||||
argKeyword argType = iota
|
||||
argFile
|
||||
argValue
|
||||
argString
|
||||
argIndeterminate
|
||||
argNode
|
||||
)
|
||||
|
||||
// commandArg specifies the type and properties of an individual argument
|
||||
type commandArg struct {
|
||||
typ argType
|
||||
required bool
|
||||
values interface{}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// string functions for the three types:
|
||||
//
|
||||
// o Commands
|
||||
// o commandArgList
|
||||
// o commandArg
|
||||
//
|
||||
// calling String() on Commands should reproduce the template from which the
|
||||
// commands were compiled
|
||||
|
||||
func (cmd Commands) String() string {
|
||||
s := strings.Builder{}
|
||||
for k, v := range cmd {
|
||||
s.WriteString(k)
|
||||
s.WriteString(fmt.Sprintf("%s", v))
|
||||
s.WriteString("\n")
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (a commandArgList) String() string {
|
||||
s := strings.Builder{}
|
||||
for i := range a {
|
||||
s.WriteString(fmt.Sprintf(" %s", a[i]))
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (c commandArg) String() string {
|
||||
switch c.typ {
|
||||
case argKeyword:
|
||||
s := "["
|
||||
switch values := c.values.(type) {
|
||||
case []string:
|
||||
for i := range values {
|
||||
s = fmt.Sprintf("%s%s|", s, values[i])
|
||||
}
|
||||
s = strings.TrimSuffix(s, "|")
|
||||
case *Commands:
|
||||
s = fmt.Sprintf("%s<commands>", s)
|
||||
default:
|
||||
s = fmt.Sprintf("%s%T", s, values)
|
||||
}
|
||||
return fmt.Sprintf("%s]", s)
|
||||
case argFile:
|
||||
return "%F"
|
||||
case argValue:
|
||||
return "%V"
|
||||
case argString:
|
||||
return "%S"
|
||||
case argIndeterminate:
|
||||
return "%*"
|
||||
}
|
||||
return "!!"
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const cycleDuration = 500 * time.Millisecond
|
||||
|
||||
// TabCompletion keeps track of the most recent tab completion attempt
|
||||
type TabCompletion struct {
|
||||
commands Commands
|
||||
|
||||
options []string
|
||||
lastOption int
|
||||
|
||||
// lastGuess is the last string generated and returned by the GuessWord
|
||||
// function. we use it to help decide whether to start a new completion
|
||||
// session
|
||||
lastGuess string
|
||||
|
||||
lastCompletionTime time.Time
|
||||
}
|
||||
|
||||
// NewTabCompletion initialises a new TabCompletion instance
|
||||
func NewTabCompletion(commands Commands) *TabCompletion {
|
||||
tc := new(TabCompletion)
|
||||
tc.commands = commands
|
||||
tc.options = make([]string, 0, len(tc.commands))
|
||||
return tc
|
||||
}
|
||||
|
||||
// GuessWord transforms the input such that the last word in the input is
|
||||
// expanded to meet the closest match in the list of allowed strings.
|
||||
func (tc *TabCompletion) GuessWord(input string) string {
|
||||
p := tokeniseInput(input)
|
||||
if len(p) == 0 {
|
||||
return input
|
||||
}
|
||||
|
||||
// if input string is the same as the string last returned by this function
|
||||
// AND it is within a time duration of 'cycleDuration' then return the next
|
||||
// option
|
||||
if input == tc.lastGuess { //&& time.Since(tc.lastCompletionTime) < cycleDuration {
|
||||
|
||||
// if there was only one option in the option list then return immediatly
|
||||
if len(tc.options) <= 1 {
|
||||
return input
|
||||
}
|
||||
|
||||
// there is more than one completion option, so shorten the input by
|
||||
// one word (getting rid of the last completion effort) and step to
|
||||
// next option
|
||||
p = p[:len(p)-1]
|
||||
tc.lastOption++
|
||||
if tc.lastOption >= len(tc.options) {
|
||||
tc.lastOption = 0
|
||||
}
|
||||
|
||||
} else {
|
||||
if strings.HasSuffix(input, " ") {
|
||||
return input
|
||||
}
|
||||
|
||||
// this is a new tabcompletion session
|
||||
tc.options = tc.options[:0]
|
||||
tc.lastOption = 0
|
||||
|
||||
// get args for command
|
||||
var arg commandArg
|
||||
|
||||
argList, ok := tc.commands[strings.ToUpper(p[0])]
|
||||
if ok && len(input) > len(p[0]) && len(argList) != 0 && len(argList) > len(p)-2 {
|
||||
arg = argList[len(p)-2]
|
||||
} else {
|
||||
arg.typ = argKeyword
|
||||
arg.values = &tc.commands
|
||||
}
|
||||
|
||||
switch arg.typ {
|
||||
case argKeyword:
|
||||
// trigger is the word we're trying to complete on
|
||||
trigger := strings.ToUpper(p[len(p)-1])
|
||||
p = p[:len(p)-1]
|
||||
|
||||
switch kw := arg.values.(type) {
|
||||
case *Commands:
|
||||
for k := range *kw {
|
||||
if len(trigger) <= len(k) && trigger == k[:len(trigger)] {
|
||||
tc.options = append(tc.options, k)
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
for _, k := range kw {
|
||||
if len(trigger) <= len(k) && trigger == k[:len(trigger)] {
|
||||
tc.options = append(tc.options, k)
|
||||
}
|
||||
}
|
||||
default:
|
||||
tc.options = append(tc.options, "unhandled argument type")
|
||||
}
|
||||
|
||||
case argFile:
|
||||
// TODO: filename completion
|
||||
tc.options = append(tc.options, "<TODO: file-completion>")
|
||||
}
|
||||
|
||||
// no completion options - return input unchanged
|
||||
if len(tc.options) == 0 {
|
||||
return input
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// add guessed word to end of input-list and rejoin to form the output
|
||||
p = append(p, tc.options[tc.lastOption])
|
||||
tc.lastGuess = strings.Join(p, " ") + " "
|
||||
|
||||
// note current time. we'll use this to help decide whether to cycle
|
||||
// through a list of options or to begin a new completion session
|
||||
tc.lastCompletionTime = time.Now()
|
||||
|
||||
return tc.lastGuess
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CommandTemplate is the root of the argument "tree"
|
||||
type CommandTemplate map[string]string
|
||||
|
||||
// CompileCommandTemplate creates a new instance of Commands from an instance
|
||||
// of CommandTemplate. if no help command is required, call the function with
|
||||
// helpKeyword == ""
|
||||
func CompileCommandTemplate(template CommandTemplate, helpKeyword string) (Commands, error) {
|
||||
var err error
|
||||
|
||||
commands := Commands{}
|
||||
for k, v := range template {
|
||||
commands[k], err = compileTemplateFragment(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error compiling %s: %s", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
if helpKeyword != "" {
|
||||
commands[helpKeyword] = commandArgList{commandArg{typ: argKeyword, required: false, values: &commands}}
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func compileTemplateFragment(fragment string) (commandArgList, error) {
|
||||
argl := commandArgList{}
|
||||
|
||||
placeholder := false
|
||||
|
||||
// loop over template string
|
||||
for i := 0; i < len(fragment); i++ {
|
||||
switch fragment[i] {
|
||||
case '%':
|
||||
placeholder = true
|
||||
|
||||
case '[':
|
||||
// find end of option list
|
||||
j := strings.LastIndex(fragment[i:], "]") + i
|
||||
if j == -1 {
|
||||
return nil, fmt.Errorf("unterminated option list")
|
||||
}
|
||||
|
||||
// check for empty list
|
||||
if i+1 == j {
|
||||
return nil, fmt.Errorf("empty option list")
|
||||
}
|
||||
|
||||
// split options list into individual options
|
||||
options := strings.Split(fragment[i+1:j], "|")
|
||||
if len(options) == 1 {
|
||||
options = make([]string, 1)
|
||||
options[0] = fragment[i+1 : j]
|
||||
}
|
||||
|
||||
// decide whether the option is a required option - if there is an
|
||||
// empty option then the option isn't required
|
||||
req := true
|
||||
for o := 0; o < len(options); o++ {
|
||||
if options[o] == "" {
|
||||
if req == false {
|
||||
return nil, fmt.Errorf("option list can contain only one empty option")
|
||||
}
|
||||
req = false
|
||||
}
|
||||
|
||||
optionParts := strings.Split(options[o], " ")
|
||||
if len(optionParts) > 1 {
|
||||
return nil, fmt.Errorf("option list can only contain single keywords (%s)", options[o])
|
||||
}
|
||||
}
|
||||
|
||||
argl = append(argl, commandArg{typ: argKeyword, required: req, values: options})
|
||||
i = j
|
||||
|
||||
case ' ':
|
||||
// skip spaces
|
||||
|
||||
default:
|
||||
if placeholder {
|
||||
switch fragment[i] {
|
||||
case 'F':
|
||||
argl = append(argl, commandArg{typ: argFile, required: true})
|
||||
case 'S':
|
||||
argl = append(argl, commandArg{typ: argString, required: true})
|
||||
case 'V':
|
||||
argl = append(argl, commandArg{typ: argValue, required: true})
|
||||
case '*':
|
||||
argl = append(argl, commandArg{typ: argIndeterminate, required: false})
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown placeholder directive (%c)", fragment[i])
|
||||
}
|
||||
placeholder = false
|
||||
i++
|
||||
} else {
|
||||
return nil, fmt.Errorf("unparsable fragment (%s)", fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return argl, nil
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopher2600/errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidateInput checks whether input is correct according to the
|
||||
// command definitions
|
||||
func (options Commands) ValidateInput(newInput *Tokens) error {
|
||||
var args commandArgList
|
||||
|
||||
tokens := newInput.tokens
|
||||
|
||||
// if tokens is empty then return
|
||||
if len(tokens) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tokens[0] = strings.ToUpper(tokens[0])
|
||||
|
||||
// basic check for whether command is recognised
|
||||
var ok bool
|
||||
if args, ok = options[tokens[0]]; !ok {
|
||||
return errors.NewFormattedError(errors.CommandError, fmt.Sprintf("%s is not a debugging command", tokens[0]))
|
||||
}
|
||||
|
||||
// too *many* arguments have been supplied
|
||||
if len(tokens)-1 > args.maximumLen() {
|
||||
return errors.NewFormattedError(errors.CommandError, fmt.Sprintf("too many arguments for %s", tokens[0]))
|
||||
}
|
||||
|
||||
// too *few* arguments have been supplied
|
||||
if len(tokens)-1 < args.requiredLen() {
|
||||
switch args[len(tokens)-1].typ {
|
||||
case argKeyword:
|
||||
return errors.NewFormattedError(errors.CommandError, fmt.Sprintf("keyword required for %s", tokens[0]))
|
||||
case argFile:
|
||||
return errors.NewFormattedError(errors.CommandError, fmt.Sprintf("filename required for %s", tokens[0]))
|
||||
case argValue:
|
||||
return errors.NewFormattedError(errors.CommandError, fmt.Sprintf("numeric argument required for %s", tokens[0]))
|
||||
case argString:
|
||||
return errors.NewFormattedError(errors.CommandError, fmt.Sprintf("string argument required for %s", tokens[0]))
|
||||
default:
|
||||
// TODO: argument types can be OR'd together. breakdown these types
|
||||
// to give more useful information
|
||||
return errors.NewFormattedError(errors.CommandError, fmt.Sprintf("too few arguments for %s", tokens[0]))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -2,7 +2,7 @@ package debugger
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"gopher2600/debugger/input"
|
||||
"gopher2600/debugger/commandline"
|
||||
"gopher2600/errors"
|
||||
"gopher2600/television"
|
||||
"strings"
|
||||
|
@ -62,7 +62,7 @@ func (trg genericTarget) FormatValue(fv interface{}) string {
|
|||
}
|
||||
|
||||
// parseTarget uses a keyword to decide which part of the vcs to target
|
||||
func parseTarget(dbg *Debugger, tokens *input.Tokens) (target, error) {
|
||||
func parseTarget(dbg *Debugger, tokens *commandline.Tokens) (target, error) {
|
||||
var trg target
|
||||
var err error
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ package debugger
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"gopher2600/debugger/commandline"
|
||||
"gopher2600/debugger/console"
|
||||
"gopher2600/debugger/input"
|
||||
"gopher2600/errors"
|
||||
"strings"
|
||||
)
|
||||
|
@ -74,7 +74,7 @@ func (tr traps) list() {
|
|||
}
|
||||
}
|
||||
|
||||
func (tr *traps) parseTrap(tokens *input.Tokens) error {
|
||||
func (tr *traps) parseTrap(tokens *commandline.Tokens) error {
|
||||
_, present := tokens.Peek()
|
||||
for present {
|
||||
tgt, err := parseTarget(tr.dbg, tokens)
|
||||
|
|
|
@ -2,8 +2,8 @@ package debugger
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"gopher2600/debugger/commandline"
|
||||
"gopher2600/debugger/console"
|
||||
"gopher2600/debugger/input"
|
||||
"gopher2600/errors"
|
||||
"gopher2600/hardware/memory"
|
||||
"strconv"
|
||||
|
@ -127,13 +127,13 @@ func (wtc *watches) list() {
|
|||
}
|
||||
}
|
||||
|
||||
func (wtc *watches) parseWatch(tokens *input.Tokens, dbgmem *memoryDebug) error {
|
||||
func (wtc *watches) parseWatch(tokens *commandline.Tokens, dbgmem *memoryDebug) error {
|
||||
var event watchEvent
|
||||
|
||||
// read mode
|
||||
mode, present := tokens.Get()
|
||||
if !present {
|
||||
return nil
|
||||
return fmt.Errorf("watch address required")
|
||||
}
|
||||
mode = strings.ToUpper(mode)
|
||||
switch mode {
|
||||
|
|
|
@ -17,10 +17,10 @@ type FormattedError struct {
|
|||
|
||||
// NewFormattedError is used to create a new instance of a FormattedError
|
||||
func NewFormattedError(errno Errno, values ...interface{}) FormattedError {
|
||||
ge := new(FormattedError)
|
||||
ge.Errno = errno
|
||||
ge.Values = values
|
||||
return *ge
|
||||
er := new(FormattedError)
|
||||
er.Errno = errno
|
||||
er.Values = values
|
||||
return *er
|
||||
}
|
||||
|
||||
func (er FormattedError) Error() string {
|
||||
|
|
Loading…
Add table
Reference in a new issue