all
This commit is contained in:
95
backend/internal/completion/service.go
Normal file
95
backend/internal/completion/service.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package completion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var ErrInvalidRequest = errors.New("invalid completion request")
|
||||
|
||||
type Request struct {
|
||||
URI string `json:"uri"`
|
||||
Text string `json:"text"`
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
Label string `json:"label"`
|
||||
Kind int `json:"kind,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
Documentation string `json:"documentation,omitempty"`
|
||||
InsertText string `json:"insertText,omitempty"`
|
||||
SortText string `json:"sortText,omitempty"`
|
||||
FilterText string `json:"filterText,omitempty"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Items []Item `json:"items"`
|
||||
IsIncomplete bool `json:"isIncomplete"`
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
DidOpen(ctx context.Context, uri, text string, version int) error
|
||||
DidChange(ctx context.Context, uri, text string, version int) error
|
||||
Completion(ctx context.Context, uri string, line, character int) (Response, error)
|
||||
}
|
||||
|
||||
type documentState struct {
|
||||
version int
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
client Client
|
||||
|
||||
mu sync.Mutex
|
||||
documents map[string]*documentState
|
||||
}
|
||||
|
||||
func NewService(client Client) *Service {
|
||||
return &Service{
|
||||
client: client,
|
||||
documents: make(map[string]*documentState),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Complete(ctx context.Context, req Request) (Response, error) {
|
||||
if err := validateRequest(req); err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
state, opened := s.documents[req.URI]
|
||||
if !opened {
|
||||
if err := s.client.DidOpen(ctx, req.URI, req.Text, 1); err != nil {
|
||||
return Response{}, fmt.Errorf("didOpen failed: %w", err)
|
||||
}
|
||||
s.documents[req.URI] = &documentState{version: 1}
|
||||
} else {
|
||||
nextVersion := state.version + 1
|
||||
if err := s.client.DidChange(ctx, req.URI, req.Text, nextVersion); err != nil {
|
||||
return Response{}, fmt.Errorf("didChange failed: %w", err)
|
||||
}
|
||||
state.version = nextVersion
|
||||
}
|
||||
|
||||
resp, err := s.client.Completion(ctx, req.URI, req.Line, req.Character)
|
||||
if err != nil {
|
||||
return Response{}, fmt.Errorf("completion failed: %w", err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func validateRequest(req Request) error {
|
||||
if req.URI == "" {
|
||||
return ErrInvalidRequest
|
||||
}
|
||||
if req.Line < 0 || req.Character < 0 {
|
||||
return ErrInvalidRequest
|
||||
}
|
||||
return nil
|
||||
}
|
||||
144
backend/internal/completion/service_test.go
Normal file
144
backend/internal/completion/service_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package completion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeLSPClient struct {
|
||||
openCalls []openCall
|
||||
changeCalls []changeCall
|
||||
completionCalls []completionCall
|
||||
completionResp Response
|
||||
openErr error
|
||||
changeErr error
|
||||
completionErr error
|
||||
}
|
||||
|
||||
type openCall struct {
|
||||
uri string
|
||||
text string
|
||||
version int
|
||||
}
|
||||
|
||||
type changeCall struct {
|
||||
uri string
|
||||
text string
|
||||
version int
|
||||
}
|
||||
|
||||
type completionCall struct {
|
||||
uri string
|
||||
line int
|
||||
character int
|
||||
}
|
||||
|
||||
func (f *fakeLSPClient) DidOpen(_ context.Context, uri, text string, version int) error {
|
||||
f.openCalls = append(f.openCalls, openCall{uri: uri, text: text, version: version})
|
||||
return f.openErr
|
||||
}
|
||||
|
||||
func (f *fakeLSPClient) DidChange(_ context.Context, uri, text string, version int) error {
|
||||
f.changeCalls = append(f.changeCalls, changeCall{uri: uri, text: text, version: version})
|
||||
return f.changeErr
|
||||
}
|
||||
|
||||
func (f *fakeLSPClient) Completion(_ context.Context, uri string, line, character int) (Response, error) {
|
||||
f.completionCalls = append(f.completionCalls, completionCall{uri: uri, line: line, character: character})
|
||||
if f.completionErr != nil {
|
||||
return Response{}, f.completionErr
|
||||
}
|
||||
return f.completionResp, nil
|
||||
}
|
||||
|
||||
func TestServiceCompleteFirstRequestSendsDidOpen(t *testing.T) {
|
||||
fake := &fakeLSPClient{
|
||||
completionResp: Response{Items: []Item{{Label: "Println"}}, IsIncomplete: true},
|
||||
}
|
||||
svc := NewService(fake)
|
||||
|
||||
resp, err := svc.Complete(context.Background(), Request{
|
||||
URI: "file:///main.go",
|
||||
Text: "package main\nfunc main() { fmt.Pr }",
|
||||
Line: 1,
|
||||
Character: 23,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Complete() error = %v", err)
|
||||
}
|
||||
if len(fake.openCalls) != 1 {
|
||||
t.Fatalf("expected 1 didOpen call, got %d", len(fake.openCalls))
|
||||
}
|
||||
if fake.openCalls[0].version != 1 {
|
||||
t.Fatalf("expected didOpen version=1, got %d", fake.openCalls[0].version)
|
||||
}
|
||||
if len(fake.changeCalls) != 0 {
|
||||
t.Fatalf("expected 0 didChange calls, got %d", len(fake.changeCalls))
|
||||
}
|
||||
if len(fake.completionCalls) != 1 {
|
||||
t.Fatalf("expected 1 completion call, got %d", len(fake.completionCalls))
|
||||
}
|
||||
if !resp.IsIncomplete || len(resp.Items) != 1 || resp.Items[0].Label != "Println" {
|
||||
t.Fatalf("unexpected response: %+v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceCompleteSecondRequestUsesDidChange(t *testing.T) {
|
||||
fake := &fakeLSPClient{}
|
||||
svc := NewService(fake)
|
||||
|
||||
_, err := svc.Complete(context.Background(), Request{
|
||||
URI: "file:///main.go",
|
||||
Text: "package main\nfunc main() {}",
|
||||
Line: 1,
|
||||
Character: 12,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("first Complete() error = %v", err)
|
||||
}
|
||||
|
||||
_, err = svc.Complete(context.Background(), Request{
|
||||
URI: "file:///main.go",
|
||||
Text: "package main\nfunc main() { fmt.Pr }",
|
||||
Line: 1,
|
||||
Character: 23,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("second Complete() error = %v", err)
|
||||
}
|
||||
|
||||
if len(fake.openCalls) != 1 {
|
||||
t.Fatalf("expected 1 didOpen call, got %d", len(fake.openCalls))
|
||||
}
|
||||
if len(fake.changeCalls) != 1 {
|
||||
t.Fatalf("expected 1 didChange call, got %d", len(fake.changeCalls))
|
||||
}
|
||||
if fake.changeCalls[0].version != 2 {
|
||||
t.Fatalf("expected didChange version=2, got %d", fake.changeCalls[0].version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceCompleteValidatesRequest(t *testing.T) {
|
||||
svc := NewService(&fakeLSPClient{})
|
||||
|
||||
_, err := svc.Complete(context.Background(), Request{URI: "", Text: "", Line: -1, Character: 0})
|
||||
if !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceCompleteReturnsClientError(t *testing.T) {
|
||||
fake := &fakeLSPClient{openErr: errors.New("open failed")}
|
||||
svc := NewService(fake)
|
||||
|
||||
_, err := svc.Complete(context.Background(), Request{
|
||||
URI: "file:///main.go",
|
||||
Text: "package main",
|
||||
Line: 0,
|
||||
Character: 0,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user