From e03cbfa79bf5941113fc2a889c313344031b4d87 Mon Sep 17 00:00:00 2001
From: qvalentin <valentin.theodor@web.de>
Date: Thu, 4 Jul 2024 17:27:20 +0200
Subject: [PATCH] perf(ast): incremental parsing of ts ast

---
 internal/lsp/ast.go           |  4 ++-
 internal/lsp/document.go      | 17 +++++++++++-
 internal/lsp/document_test.go | 52 +++++++++++++++++++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)

diff --git a/internal/lsp/ast.go b/internal/lsp/ast.go
index c258829b..e918b00b 100644
--- a/internal/lsp/ast.go
+++ b/internal/lsp/ast.go
@@ -8,6 +8,7 @@ import (
 	lsp "go.lsp.dev/protocol"
 )
 
+// TODO: use byte instead of string
 func ParseAst(oldTree *sitter.Tree, content string) *sitter.Tree {
 	parser := sitter.NewParser()
 	parser.SetLanguage(gotemplate.GetLanguage())
@@ -69,7 +70,8 @@ func isPointLargerOrEq(a sitter.Point, b sitter.Point) bool {
 	return a.Row > b.Row
 }
 
-func (d *Document) ApplyChangesToAst(newContent string) {
+func (d *Document) ApplyChangesToAst(editInput sitter.EditInput, newContent string) {
+	d.Ast.Edit(editInput)
 	d.Ast = ParseAst(nil, newContent)
 }
 
diff --git a/internal/lsp/document.go b/internal/lsp/document.go
index 70265950..02cd51cb 100644
--- a/internal/lsp/document.go
+++ b/internal/lsp/document.go
@@ -35,15 +35,30 @@ func (d *Document) ApplyChanges(changes []lsp.TextDocumentContentChangeEvent) {
 	for _, change := range changes {
 		start, end := util.PositionToIndex(change.Range.Start, content), util.PositionToIndex(change.Range.End, content)
 
+		newEnd := start + len(change.Text)
+
 		var buf bytes.Buffer
+
 		buf.Write(content[:start])
 		buf.Write([]byte(change.Text))
 		buf.Write(content[end:])
+
 		content = buf.Bytes()
+
+		editInput := sitter.EditInput{
+			StartIndex:  uint32(start),
+			OldEndIndex: uint32(end),
+			NewEndIndex: uint32(newEnd),
+			StartPoint:  util.PositionToPoint(change.Range.Start),
+			OldEndPoint: util.PositionToPoint(change.Range.End),
+			NewEndPoint: util.PositionToPoint(lsp.Position{Line: change.Range.Start.Line, Character: change.Range.Start.Character + uint32(len(change.Text))}),
+		}
+
+		d.Ast.Edit(editInput)
 	}
 	d.Content = string(content)
 
-	d.ApplyChangesToAst(d.Content)
+	d.Ast = ParseAst(d.Ast, string(content))
 	d.SymbolTable = NewSymbolTable(d.Ast, []byte(d.Content))
 
 	d.lines = nil
diff --git a/internal/lsp/document_test.go b/internal/lsp/document_test.go
index c09d0ca3..6ff2fa86 100644
--- a/internal/lsp/document_test.go
+++ b/internal/lsp/document_test.go
@@ -35,3 +35,55 @@ func TestDocumentStore(t *testing.T) {
 	assert.NotNil(doc)
 	assert.True(ok)
 }
+
+func TestApplyChanges(t *testing.T) {
+	assert := assert.New(t)
+
+	documentStore := NewDocumentStore()
+	documentStore.DidOpen(&protocol.DidOpenTextDocumentParams{
+		TextDocument: protocol.TextDocumentItem{
+			URI:        uri.File("test.yaml"),
+			LanguageID: "helm",
+			Text:       `{{ .Values.test }}`,
+		},
+	}, util.DefaultConfig)
+
+	doc, ok := documentStore.Get(uri.File("test.yaml"))
+	assert.True(ok)
+	assert.Equal("{{ .Values.test }}", doc.Content)
+
+	doc.ApplyChanges([]protocol.TextDocumentContentChangeEvent{
+		{Range: protocol.Range{Start: protocol.Position{Line: 0, Character: 18}, End: protocol.Position{Line: 0, Character: 18}}, Text: "\n"},
+		{Range: protocol.Range{Start: protocol.Position{Line: 1, Character: 0}, End: protocol.Position{Line: 1, Character: 0}}, Text: "\n"},
+		{Range: protocol.Range{Start: protocol.Position{Line: 1, Character: 0}, End: protocol.Position{Line: 1, Character: 0}}, Text: "spec:\n  replicas: {{ .Values.replicaCount }}\n  selector:\n    matchLabels:\n      {{- include \"hello-world.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      labels:"},
+		{Range: protocol.Range{Start: protocol.Position{Line: 8, Character: 13}, End: protocol.Position{Line: 9, Character: 0}}, Text: "\n      \n"},
+		{Range: protocol.Range{Start: protocol.Position{Line: 9, Character: 6}, End: protocol.Position{Line: 9, Character: 0}}, Text: "{{- if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ include \"hello-world.serviceAccountName\" . }}\n  labels:\n    {{- include \"hello-world.labels\" . | nindent 4 }}\n  {{- with .Values.serviceAccount.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\n{{- end }}"},
+		{Range: protocol.Range{Start: protocol.Position{Line: 17, Character: 0}, End: protocol.Position{Line: 17, Character: 0}}, Text: ""},
+		{Range: protocol.Range{Start: protocol.Position{Line: 18, Character: 0}, End: protocol.Position{Line: 19, Character: 0}}, Text: ""},
+	})
+
+	print(doc.Content)
+	expected := `{{ .Values.test }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      {{- include "hello-world.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      labels:
+      {{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ include "hello-world.serviceAccountName" . }}
+  labels:
+    {{- include "hello-world.labels" . | nindent 4 }}
+  {{- with .Values.serviceAccount.annotations }}
+  annotations:
+  {{- end }}
+{{- end }}
+`
+
+	assert.Equal(expected, doc.Content)
+}