internal/lsp/template: improve error and quote handling

Template tokenization was handling quoting incorrectly.
(The previous code would misunderstand {{"{{"}}.)

While the user is typing an action in a template file the template
parser was returning a correct but useless error, so gopls had no
information about the file. The new code improves this by tokenizing
and parsing an adjusted version of the file. That is, it can sometimes
tell when the user has started typing an action so the {{, }} delimiters
are unbalanced. It replaces the new {{ by blanks, thereby suppressing
a lot of useless error messages from gopls.

Something like this seems to be a prerequisite for provdiding completions
based on the contents of the rest of the file.

Change-Id: I6b6396a4d9e599d671e778b303e6628642585a90
Reviewed-on: https://go-review.googlesource.com/c/tools/+/337351
Run-TryBot: Peter Weinberger <pjw@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
Trust: Peter Weinberger <pjw@google.com>
diff --git a/internal/lsp/template/parse.go b/internal/lsp/template/parse.go
index 0853612..a4891f6 100644
--- a/internal/lsp/template/parse.go
+++ b/internal/lsp/template/parse.go
@@ -116,13 +116,14 @@
 		for t == nil && ans.ParseErr == nil {
 			//  template: :2: function "foo" not defined
 			matches := parseErrR.FindStringSubmatch(err.Error())
-			if len(matches) < 2 { // uncorrectable error
-				ans.ParseErr = err
-				return ans
+			if len(matches) == 2 {
+				// suppress the error by giving it a function with the right name
+				funcs[matches[1]] = func() interface{} { return nil }
+				t, err = template.New("").Funcs(funcs).Parse(string(buf))
+				continue
 			}
-			// suppress the error by giving it a function with the right name
-			funcs[matches[1]] = func(interface{}) interface{} { return nil }
-			t, err = template.New("").Funcs(funcs).Parse(string(buf))
+			ans.ParseErr = err // unfixed error
+			return ans
 		}
 	}
 	ans.named = t.Templates()
@@ -173,24 +174,85 @@
 	return left + 1, right - left - 1
 }
 
-var parseErrR = regexp.MustCompile(`template:.*function "([^"]+)" not defined`)
+var (
+	parseErrR = regexp.MustCompile(`template:.*function "([^"]+)" not defined`)
+)
 
 func (p *Parsed) setTokens() {
-	last := 0
-	for left := bytes.Index(p.buf[last:], Left); left != -1; left = bytes.Index(p.buf[last:], Left) {
-		left += last
-		tok := Token{Start: left}
-		last = left + len(Left)
-		right := bytes.Index(p.buf[last:], Right)
-		if right == -1 {
-			break
+	const (
+		// InRaw and InString only occur inside an action (SeenLeft)
+		Start = iota
+		InRaw
+		InString
+		SeenLeft
+	)
+	state := Start
+	var left, oldState int
+	for n := 0; n < len(p.buf); n++ {
+		c := p.buf[n]
+		switch state {
+		case InRaw:
+			if c == '`' {
+				state = oldState
+			}
+		case InString:
+			if c == '"' && !isEscaped(p.buf[:n]) {
+				state = oldState
+			}
+		case SeenLeft:
+			if c == '`' {
+				oldState = state // it's SeenLeft, but a little clearer this way
+				state = InRaw
+				continue
+			}
+			if c == '"' {
+				oldState = state
+				state = InString
+				continue
+			}
+			if bytes.HasPrefix(p.buf[n:], Right) {
+				right := n + len(Right)
+				tok := Token{Start: left,
+					End:       right,
+					Multiline: bytes.Contains(p.buf[left:right], []byte{'\n'}),
+				}
+				p.tokens = append(p.tokens, tok)
+				state = Start
+			}
+			// If we see (unquoted) Left then the original left is probably the user
+			// typing. Suppress the original left
+			if bytes.HasPrefix(p.buf[n:], Left) {
+				for i := 0; i < len(Left); i++ {
+					p.buf[left+i] = ' '
+				}
+				left = n
+				n += len(Left) - 1 // skip the rest
+			}
+		case Start:
+			if bytes.HasPrefix(p.buf[n:], Left) {
+				left = n
+				state = SeenLeft
+				n += len(Left) - 1 // skip the rest (avoids {{{ bug)
+			}
 		}
-		right += last + len(Right)
-		tok.End = right
-		tok.Multiline = bytes.Contains(p.buf[left:right], []byte{'\n'})
-		p.tokens = append(p.tokens, tok)
-		last = right
 	}
+	// this error occurs after typing {{ at the end of the file
+	if state != Start {
+		log.Printf("state is %d", state)
+		// Unclosed Left. remove the Left at left
+		for i := 0; i < len(Left); i++ {
+			p.buf[left+i] = ' '
+		}
+	}
+}
+
+// isEscaped reports whether the byte after buf is escaped
+func isEscaped(buf []byte) bool {
+	backSlashes := 0
+	for j := len(buf) - 1; j >= 0 && buf[j] == '\\'; j-- {
+		backSlashes++
+	}
+	return backSlashes%2 == 1
 }
 
 func (p *Parsed) Tokens() []Token {
diff --git a/internal/lsp/template/parse_test.go b/internal/lsp/template/parse_test.go
index e6a95ef..afa76d1 100644
--- a/internal/lsp/template/parse_test.go
+++ b/internal/lsp/template/parse_test.go
@@ -190,3 +190,28 @@
 		t.Error("expected nonASCII to be true")
 	}
 }
+
+type ttest struct {
+	tmpl   string
+	tokCnt int
+}
+
+func TestQuotes(t *testing.T) {
+	tsts := []ttest{
+		{"{{- /*comment*/ -}}", 1},
+		{"{{/*`\ncomment\n`*/}}", 1},
+		//{"{{foo\nbar}}\n", 1}, // this action spanning lines parses in 1.16
+		{"{{\"{{foo}}{{\"}}", 1},
+		{"{{\n{{- when}}", 1},      // corrected
+		{"{{{{if .}}xx{{end}}", 2}, // corrected
+	}
+	for _, s := range tsts {
+		p := parseBuffer([]byte(s.tmpl))
+		if len(p.tokens) != s.tokCnt {
+			t.Errorf("%q: got %d tokens, expected %d", s, len(p.tokens), s.tokCnt)
+		}
+		if p.ParseErr != nil {
+			t.Errorf("%q: %v", string(p.buf), p.ParseErr)
+		}
+	}
+}