<commit_before>/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Package languageserver provides an implementation of the Language Server
// Protocol v3.0 (https://github.com/Microsoft/language-server-protocol)
// This server implements the following capabilities:
// 		textDocumentSync (full)
//		referenceProvider
package languageserver

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"strings"

	"kythe.io/kythe/go/services/xrefs"
	"kythe.io/kythe/go/util/kytheuri"
	"kythe.io/kythe/go/util/markedsource"

	cpb "kythe.io/kythe/proto/common_go_proto"
	xpb "kythe.io/kythe/proto/xref_go_proto"

	"github.com/sourcegraph/go-langserver/pkg/lsp"
)

// Server provides a Language Server for interacting with data in a Kythe index
type Server struct {
	workspaces []Workspace
	docs       map[LocalFile]*document
	XRefs      xrefs.Service
	opts       *Options
}

// Options control optional behaviours of the language server implementation.
type Options struct {
	// The number of cross-references the server will request by default.
	// If ≤ 0, a reasonable default will be chosen.
	PageSize int

	// If set, this function will be called to produce a workspace for the
	// given LSP document. If unset, uses NewSettingsWorkspaceFromURI.
	NewWorkspace func(lsp.DocumentURI) (Workspace, error)
}

func (o *Options) pageSize() int {
	if o == nil || o.PageSize <= 0 {
		return 500
	}
	return o.PageSize
}

func (o *Options) newWorkspace(u lsp.DocumentURI) (Workspace, error) {
	if o == nil || o.NewWorkspace == nil {
		return NewSettingsWorkspaceFromURI(u)
	}
	return o.NewWorkspace(u)
}

// NewServer constructs a server that delegates cross-reference requests to the
// specified xrefs implementation. If opts == nil, sensible defaults are used.
func NewServer(xrefs xrefs.Service, opts *Options) Server {
	return Server{
		docs:       make(map[LocalFile]*document),
		workspaces: nil,
		XRefs:      xrefs,
		opts:       opts,
	}
}

// Initialize is invoked before any other methods, and allows the Server to
// receive configuration info (such as the project root) and announce its capabilities.
func (ls *Server) Initialize(params lsp.InitializeParams) (*lsp.InitializeResult, error) {
	log.Println("Server Initializing...")

	fullSync := lsp.TDSKFull
	return &lsp.InitializeResult{
		Capabilities: lsp.ServerCapabilities{
			TextDocumentSync: lsp.TextDocumentSyncOptionsOrKind{
				Kind:    &fullSync,
				Options: nil,
			},
			ReferencesProvider: true,
			HoverProvider:      true,
			DefinitionProvider: true,
		},
	}, nil
}

// TextDocumentDidOpen allows the client to inform the Server that a file has
// been opened. The Kythe Language Server uses this time to fetch file
// decorations.
func (ls *Server) TextDocumentDidOpen(params lsp.DidOpenTextDocumentParams) error {
	log.Printf("Opened file: %q", params.TextDocument.URI)

	local, err := ls.localFromURI(params.TextDocument.URI)
	if err != nil {
		return fmt.Errorf("failed creating local path from URI %q:\n%v", params.TextDocument.URI, err)

	}

	log.Printf("Found %q in workspace %q", local.RelativePath, local.Workspace.Root())

	ticket, err := local.KytheURI()
	if err != nil {
		return err
	}

	dec, err := ls.XRefs.Decorations(context.TODO(), &xpb.DecorationsRequest{
		Location: &xpb.Location{
			Ticket: ticket.String(),
		},
		References:        true,
		TargetDefinitions: true,
		SourceText:        true,
	})

	if err != nil {
		return fmt.Errorf("failed to find xrefs for %q:\n%v", local, err)
	}

	log.Printf("Server returned %d refs in file %q", len(dec.Reference), local)

	var refs []*RefResolution
	for _, r := range dec.Reference {
		if r.Span == nil {
			continue
		}

		rng := spanToRange(r.Span)
		if rng == nil {
			continue
		}

		refs = append(refs, &RefResolution{
			ticket:   r.TargetTicket,
			def:      r.TargetDefinition,
			oldRange: *rng,
		})
	}

	defLocs := ls.defLocations(local.Workspace, dec.DefinitionLocations)
	log.Printf("Found %d defs in file %q", len(defLocs), ticket.String())
	log.Printf("Found %d refs in file %q", len(refs), ticket.String())
	ls.docs[local] = newDocument(refs, string(dec.SourceText), params.TextDocument.Text, defLocs)
	log.Printf("Currently opened: %d files", len(ls.docs))

	return nil
}

// TextDocumentDidChange is called when the client edits a file. The Kythe
// Language Server simply stores the new content and marks the file as dirty
func (ls *Server) TextDocumentDidChange(params lsp.DidChangeTextDocumentParams) error {
	local, err := ls.localFromURI(params.TextDocument.URI)
	if err != nil {
		return err
	} else if len(params.ContentChanges) == 0 {
		// This should not be possible, since we request full sync, but some
		// plugins appear not to respect this in every case.  Treat this
		// similarly to a file that was never opened.
		return fmt.Errorf("unexpectedly empty change update on %q", params.TextDocument.URI)
	}

	if doc, ok := ls.docs[local]; ok {
		// Because the sync Kind is full text, each change notification
		// contains exactly 1 change with the full text of the document
		doc.updateSource(params.ContentChanges[0].Text)
	} else {
		return fmt.Errorf("change notification received for file that was never opened %q", local)
	}

	return nil
}

// TextDocumentDidClose removes all cached information about the open document. Because
// all information extracted from documents are stored internally to the document object,
// this removal shouldn't leak memory
func (ls *Server) TextDocumentDidClose(params lsp.DidCloseTextDocumentParams) error {
	log.Printf("Document close notification received: %v", params)
	local, err := ls.localFromURI(params.TextDocument.URI)
	if err != nil {
		return err
	}

	delete(ls.docs, local)
	return nil
}

// TextDocumentReferences uses a position in code to produce a list of
// locations throughout the project that reference the same semantic node. This
// can trigger a diff if the source file is dirty.
//<commit_msg>KLS: Fall back to document open on non-matching changes.<commit_after>/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Package languageserver provides an implementation of the Language Server
// Protocol v3.0 (https://github.com/Microsoft/language-server-protocol)
// This server implements the following capabilities:
// 		textDocumentSync (full)
//		referenceProvider
package languageserver

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"strings"

	"kythe.io/kythe/go/services/xrefs"
	"kythe.io/kythe/go/util/kytheuri"
	"kythe.io/kythe/go/util/markedsource"

	cpb "kythe.io/kythe/proto/common_go_proto"
	xpb "kythe.io/kythe/proto/xref_go_proto"

	"github.com/sourcegraph/go-langserver/pkg/lsp"
)

// Server provides a Language Server for interacting with data in a Kythe index
type Server struct {
	workspaces []Workspace
	docs       map[LocalFile]*document
	XRefs      xrefs.Service
	opts       *Options
}

// Options control optional behaviours of the language server implementation.
type Options struct {
	// The number of cross-references the server will request by default.
	// If ≤ 0, a reasonable default will be chosen.
	PageSize int

	// If set, this function will be called to produce a workspace for the
	// given LSP document. If unset, uses NewSettingsWorkspaceFromURI.
	NewWorkspace func(lsp.DocumentURI) (Workspace, error)
}

func (o *Options) pageSize() int {
	if o == nil || o.PageSize <= 0 {
		return 500
	}
	return o.PageSize
}

func (o *Options) newWorkspace(u lsp.DocumentURI) (Workspace, error) {
	if o == nil || o.NewWorkspace == nil {
		return NewSettingsWorkspaceFromURI(u)
	}
	return o.NewWorkspace(u)
}

// NewServer constructs a server that delegates cross-reference requests to the
// specified xrefs implementation. If opts == nil, sensible defaults are used.
func NewServer(xrefs xrefs.Service, opts *Options) Server {
	return Server{
		docs:       make(map[LocalFile]*document),
		workspaces: nil,
		XRefs:      xrefs,
		opts:       opts,
	}
}

// Initialize is invoked before any other methods, and allows the Server to
// receive configuration info (such as the project root) and announce its capabilities.
func (ls *Server) Initialize(params lsp.InitializeParams) (*lsp.InitializeResult, error) {
	log.Println("Server Initializing...")

	fullSync := lsp.TDSKFull
	return &lsp.InitializeResult{
		Capabilities: lsp.ServerCapabilities{
			TextDocumentSync: lsp.TextDocumentSyncOptionsOrKind{
				Kind:    &fullSync,
				Options: nil,
			},
			ReferencesProvider: true,
			HoverProvider:      true,
			DefinitionProvider: true,
		},
	}, nil
}

// TextDocumentDidOpen allows the client to inform the Server that a file has
// been opened. The Kythe Language Server uses this time to fetch file
// decorations.
func (ls *Server) TextDocumentDidOpen(params lsp.DidOpenTextDocumentParams) error {
	log.Printf("Opened file: %q", params.TextDocument.URI)

	local, err := ls.localFromURI(params.TextDocument.URI)
	if err != nil {
		return fmt.Errorf("failed creating local path from URI %q:\n%v", params.TextDocument.URI, err)

	}

	log.Printf("Found %q in workspace %q", local.RelativePath, local.Workspace.Root())

	ticket, err := local.KytheURI()
	if err != nil {
		return err
	}

	dec, err := ls.XRefs.Decorations(context.TODO(), &xpb.DecorationsRequest{
		Location: &xpb.Location{
			Ticket: ticket.String(),
		},
		References:        true,
		TargetDefinitions: true,
		SourceText:        true,
	})

	if err != nil {
		return fmt.Errorf("failed to find xrefs for %q:\n%v", local, err)
	}

	log.Printf("Server returned %d refs in file %q", len(dec.Reference), local)

	var refs []*RefResolution
	for _, r := range dec.Reference {
		if r.Span == nil {
			continue
		}

		rng := spanToRange(r.Span)
		if rng == nil {
			continue
		}

		refs = append(refs, &RefResolution{
			ticket:   r.TargetTicket,
			def:      r.TargetDefinition,
			oldRange: *rng,
		})
	}

	defLocs := ls.defLocations(local.Workspace, dec.DefinitionLocations)
	log.Printf("Found %d defs in file %q", len(defLocs), ticket.String())
	log.Printf("Found %d refs in file %q", len(refs), ticket.String())
	ls.docs[local] = newDocument(refs, string(dec.SourceText), params.TextDocument.Text, defLocs)
	log.Printf("Currently opened: %d files", len(ls.docs))

	return nil
}

// TextDocumentDidChange is called when the client edits a file. The Kythe
// Language Server simply stores the new content and marks the file as dirty
func (ls *Server) TextDocumentDidChange(params lsp.DidChangeTextDocumentParams) error {
	local, err := ls.localFromURI(params.TextDocument.URI)
	if err != nil {
		return err
	} else if len(params.ContentChanges) == 0 {
		// This should not be possible, since we request full sync, but some
		// plugins appear not to respect this in every case.  Treat this
		// similarly to a file that was never opened.
		return fmt.Errorf("unexpectedly empty change update on %q", params.TextDocument.URI)
	}

	if doc, ok := ls.docs[local]; ok {
		// Because the sync Kind is full text, each change notification
		// contains exactly 1 change with the full text of the document
		doc.updateSource(params.ContentChanges[0].Text)
		return nil
	}

	// If we get here, we were passed a change for a file that was never
	// opened.  That might be true, or it might be that the server restarted
	// and lost its context. Give the caller the benefit of the doubt and
	// attempt to open the file.
	log.Printf("No matching open document found for %q; attempting to open instead", params.TextDocument.URI)
	if err := ls.TextDocumentDidOpen(lsp.DidOpenTextDocumentParams{
		TextDocument: lsp.TextDocumentItem{URI: params.TextDocument.URI},
	}); err != nil {
		return err
	}

	// Assuming that worked, process the change.
	if doc, ok := ls.docs[local]; ok {
		doc.updateSource(params.ContentChanges[0].Text)
		return nil
	}
	return fmt.Errorf("change notification received for file that was never opened %q", local)
}

// TextDocumentDidClose removes all cached information about the open document. Because
// all information extracted from documents are stored internally to the document object,
// this removal shouldn't leak memory
func (ls *Server) TextDocumentDidClose(params lsp.DidCloseTextDocumentParams) error {
	log.Printf("Document close notification received: %v", params)
	local, err := ls.localFromURI(params.TextDocument.URI)
	if err != nil {
		return err
	}

	delete(ls.docs, local)
	return nil
}

// TextDocumentReferences uses a position in code to produce a list of
// locations throughout the project that reference the same semantic node. This
// can trigger a diff if the source file is dirty.
//
{'fraction_non_alphanumeric': 0.10139834541328062, 'fraction_numerical': 0.0027088366644703126, 'mean_word_length': 3.901327592393254, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 3, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4428520', 'n_tokens_mistral': 4318, 'n_tokens_neox': 4089, 'n_words': 1461}