internal/lsp: enable incrementalSync by default

This change also leaves in an opt-out setting (noIncrementalSync), just
in case we need to disable it at some point.

Change-Id: I3575efe942294b764c35d9259ce75d124b590e98
Reviewed-on: https://go-review.googlesource.com/c/tools/+/182468
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/diff/diff.go b/internal/lsp/diff/diff.go
index a789a7f..cf7158d 100644
--- a/internal/lsp/diff/diff.go
+++ b/internal/lsp/diff/diff.go
@@ -200,15 +200,14 @@
 
 			// Return if we've exceeded the maximum values.
 			if x == M && y == N {
-				// Save the state of the array, and exit function
+				// Makes sure to save the state of the array before returning.
 				copy(copyV, V)
 				trace[d] = copyV
-
 				return trace, offset
 			}
 		}
 
-		// Save the state of the array, and continue loop
+		// Save the state of the array.
 		copy(copyV, V)
 		trace[d] = copyV
 	}
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 683e537..1e1af99 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -26,11 +26,11 @@
 	}
 	s.isInitialized = true // mark server as initialized now
 
-	// TODO(iancottrell): Change this default to protocol.Incremental and remove the option
-	s.textDocumentSyncKind = protocol.Full
+	// TODO: Remove the option once we are certain there are no issues here.
+	s.textDocumentSyncKind = protocol.Incremental
 	if opts, ok := params.InitializationOptions.(map[string]interface{}); ok {
-		if opt, ok := opts["incrementalSync"].(bool); ok && opt {
-			s.textDocumentSyncKind = protocol.Incremental
+		if opt, ok := opts["noIncrementalSync"].(bool); ok && opt {
+			s.textDocumentSyncKind = protocol.Full
 		}
 	}
 
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index b20b1a2..fdb424c 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -7,6 +7,7 @@
 import (
 	"bytes"
 	"context"
+	"fmt"
 
 	"golang.org/x/tools/internal/jsonrpc2"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -25,23 +26,29 @@
 		return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided")
 	}
 
-	var text string
-	switch s.textDocumentSyncKind {
-	case protocol.Incremental:
-		var err error
-		text, err = s.applyChanges(ctx, params)
-		if err != nil {
-			return err
+	uri := span.NewURI(params.TextDocument.URI)
+
+	// Check if the client sent the full content of the file.
+	// We accept a full content change even if the server expected incremental changes.
+	text, isFullChange := fullChange(params.ContentChanges)
+
+	// We only accept an incremental change if the server expected it.
+	if !isFullChange {
+		switch s.textDocumentSyncKind {
+		case protocol.Full:
+			return fmt.Errorf("expected a full content change, received incremental changes for %s", uri)
+		case protocol.Incremental:
+			// Determine the new file content.
+			var err error
+			text, err = s.applyChanges(ctx, uri, params.ContentChanges)
+			if err != nil {
+				return err
+			}
 		}
-	case protocol.Full:
-		// We expect the full content of file, i.e. a single change with no range.
-		change := params.ContentChanges[0]
-		if change.RangeLength != 0 {
-			return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
-		}
-		text = change.Text
 	}
-	return s.cacheAndDiagnose(ctx, span.NewURI(params.TextDocument.URI), []byte(text))
+
+	// Cache the new file content and send fresh diagnostics.
+	return s.cacheAndDiagnose(ctx, uri, []byte(text))
 }
 
 func (s *Server) cacheAndDiagnose(ctx context.Context, uri span.URI, content []byte) error {
@@ -56,23 +63,24 @@
 	return nil
 }
 
-func (s *Server) applyChanges(ctx context.Context, params *protocol.DidChangeTextDocumentParams) (string, error) {
-	if len(params.ContentChanges) == 1 && params.ContentChanges[0].Range == nil {
-		// If range is empty, we expect the full content of file, i.e. a single change with no range.
-		change := params.ContentChanges[0]
-		if change.RangeLength != 0 {
-			return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
-		}
-		return change.Text, nil
+func fullChange(changes []protocol.TextDocumentContentChangeEvent) (string, bool) {
+	if len(changes) > 1 {
+		return "", false
 	}
+	// The length of the changes must be 1 at this point.
+	if changes[0].Range == nil && changes[0].RangeLength == 0 {
+		return changes[0].Text, true
+	}
+	return "", false
+}
 
-	uri := span.NewURI(params.TextDocument.URI)
+func (s *Server) applyChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) (string, error) {
 	content, _, err := s.session.GetFile(uri).Read(ctx)
 	if err != nil {
 		return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
 	}
 	fset := s.session.Cache().FileSet()
-	for _, change := range params.ContentChanges {
+	for _, change := range changes {
 		// Update column mapper along with the content.
 		m := protocol.NewColumnMapper(uri, uri.Filename(), fset, nil, content)