Skip to content

Commit

Permalink
lsp: Correctly handle URIs and paths on Windows (#569)
Browse files Browse the repository at this point in the history
* lsp: Correctly handle URIs and paths on Windows

Signed-off-by: Charlie Egan <[email protected]>

* Appease linter

Signed-off-by: Charlie Egan <[email protected]>

* Add tests for generic client URI behaviour

Signed-off-by: Charlie Egan <[email protected]>

---------

Signed-off-by: Charlie Egan <[email protected]>
  • Loading branch information
charlieegan3 authored Feb 21, 2024
1 parent f11eef4 commit ece9977
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 42 deletions.
14 changes: 2 additions & 12 deletions internal/lsp/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package lsp

import (
"fmt"
"net/url"
"os"
"sync"

Expand Down Expand Up @@ -196,17 +195,8 @@ func (c *Cache) Delete(uri string) {
c.diagnosticsParseMu.Unlock()
}

func updateCacheForURIFromDisk(cache *Cache, uri string) (string, error) {
parsedURI, err := url.Parse(uri)
if err != nil {
return "", fmt.Errorf("failed to parse URI: %w", err)
}

if parsedURI.Scheme != "file" {
return "", fmt.Errorf("only file:// URIs are supported, got %q", parsedURI.String())
}

content, err := os.ReadFile(parsedURI.Path)
func updateCacheForURIFromDisk(cache *Cache, uri, path string) (string, error) {
content, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("failed to read file: %w", err)
}
Expand Down
18 changes: 18 additions & 0 deletions internal/lsp/clients/clients.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package clients

// Identifier represent different supported clients and can be used to toggle or change
// server behavior based on the client.
type Identifier int

const (
IdentifierGeneric Identifier = iota
IdentifierVSCode
)

func DetermineClientIdentifier(clientName string) Identifier {
if clientName == "Visual Studio Code" {
return IdentifierVSCode
}

return IdentifierGeneric
}
9 changes: 9 additions & 0 deletions internal/lsp/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ type TextDocumentIdentifier struct {
URI string `json:"uri"`
}

type TextDocumentDidChangeParams struct {
TextDocument TextDocumentIdentifier `json:"textDocument"`
ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"`
}

type TextDocumentContentChangeEvent struct {
Text string `json:"text"`
}

type Diagnostic struct {
Range Range `json:"range"`
Message string `json:"message"`
Expand Down
67 changes: 37 additions & 30 deletions internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strings"
Expand All @@ -15,6 +14,8 @@ import (
"github.com/sourcegraph/jsonrpc2"
"gopkg.in/yaml.v3"

"github.com/styrainc/regal/internal/lsp/clients"
"github.com/styrainc/regal/internal/lsp/uri"
"github.com/styrainc/regal/pkg/config"
)

Expand Down Expand Up @@ -51,7 +52,16 @@ type LanguageServer struct {
diagnosticRequestFile chan fileDiagnosticRequiredEvent
diagnosticRequestWorkspace chan string

clientRootURI string
clientRootURI string
clientIdentifier clients.Identifier
}

// fileDiagnosticRequiredEvent is sent to the diagnosticRequestFile channel when
// diagnostics are required for a file.
type fileDiagnosticRequiredEvent struct {
Reason string
URI string
Content string
}

func (l *LanguageServer) Handle(
Expand Down Expand Up @@ -256,14 +266,6 @@ func (l *LanguageServer) logOutboundMessage(method string, message any) {
}
}

// fileDiagnosticRequiredEvent is sent to the diagnosticRequestFile channel when
// diagnostics are required for a file.
type fileDiagnosticRequiredEvent struct {
Reason string
URI string
Content string
}

func (l *LanguageServer) handleTextDocumentDidOpen(
_ context.Context,
_ *jsonrpc2.Conn,
Expand All @@ -283,15 +285,6 @@ func (l *LanguageServer) handleTextDocumentDidOpen(
return struct{}{}, nil
}

type TextDocumentDidChangeParams struct {
TextDocument TextDocumentIdentifier `json:"textDocument"`
ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"`
}

type TextDocumentContentChangeEvent struct {
Text string `json:"text"`
}

func (l *LanguageServer) handleTextDocumentDidChange(
_ context.Context,
_ *jsonrpc2.Conn,
Expand Down Expand Up @@ -322,7 +315,11 @@ func (l *LanguageServer) handleWorkspaceDidCreateFiles(
}

for _, createOp := range params.Files {
_, err = updateCacheForURIFromDisk(l.cache, createOp.URI)
_, err = updateCacheForURIFromDisk(
l.cache,
uri.FromPath(l.clientIdentifier, createOp.URI),
uri.ToPath(l.clientIdentifier, createOp.URI),
)
if err != nil {
return nil, fmt.Errorf("failed to update cache for uri %q: %w", createOp.URI, err)
}
Expand Down Expand Up @@ -369,7 +366,11 @@ func (l *LanguageServer) handleWorkspaceDidRenameFiles(
}

for _, renameOp := range params.Files {
content, err := updateCacheForURIFromDisk(l.cache, renameOp.NewURI)
content, err := updateCacheForURIFromDisk(
l.cache,
uri.FromPath(l.clientIdentifier, renameOp.NewURI),
uri.ToPath(l.clientIdentifier, renameOp.NewURI),
)
if err != nil {
return nil, fmt.Errorf("failed to update cache for uri %q: %w", renameOp.NewURI, err)
}
Expand Down Expand Up @@ -418,6 +419,13 @@ func (l *LanguageServer) handleInitialize(
}

l.clientRootURI = params.RootURI
l.clientIdentifier = clients.DetermineClientIdentifier(params.ClientInfo.Name)

if l.clientIdentifier == clients.IdentifierGeneric {
l.log(
"Unable to match client identifier for initializing client, using generic functionality: " + params.ClientInfo.Name,
)
}

regoFilter := FileOperationFilter{
Scheme: "file",
Expand Down Expand Up @@ -453,36 +461,35 @@ func (l *LanguageServer) handleInitialize(
},
}

folderURI, err := url.Parse(l.clientRootURI)
if err != nil {
return nil, fmt.Errorf("failed to parse URI: %w", err)
}
workspaceRootPath := uri.ToPath(l.clientIdentifier, l.clientRootURI)

// load the rego source files into the cache
err = filepath.WalkDir(folderURI.Path, func(path string, d os.DirEntry, err error) error {
err = filepath.WalkDir(workspaceRootPath, func(path string, d os.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("failed to walk workspace dir %q: %w", folderURI.Path, err)
return fmt.Errorf("failed to walk workspace dir %q: %w", d.Name(), err)
}

// TODO(charlieegan3): make this configurable for things like .rq etc?
if d.IsDir() || !strings.HasSuffix(path, ".rego") {
return nil
}

_, err = updateCacheForURIFromDisk(l.cache, "file://"+path)
fileURI := uri.FromPath(l.clientIdentifier, path)

_, err = updateCacheForURIFromDisk(l.cache, fileURI, path)
if err != nil {
return fmt.Errorf("failed to update cache for uri %q: %w", path, err)
}

_, err = updateParse(l.cache, "file://"+path)
_, err = updateParse(l.cache, fileURI)
if err != nil {
return fmt.Errorf("failed to update parse: %w", err)
}

return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk workspace dir %q: %w", folderURI.Path, err)
return nil, fmt.Errorf("failed to walk workspace dir %q: %w", workspaceRootPath, err)
}

// attempt to load the config as it is found on disk
Expand Down
45 changes: 45 additions & 0 deletions internal/lsp/uri/uri.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package uri

import (
"path/filepath"
"strings"

"github.com/styrainc/regal/internal/lsp/clients"
)

// FromPath converts a file path to a URI for a given client.
// Since clients expect URIs to be in a specific format, this function
// will convert the path to the appropriate format for the client.
func FromPath(client clients.Identifier, path string) string {
path = strings.TrimPrefix(path, "file://")
path = strings.TrimPrefix(path, "/")

if client == clients.IdentifierVSCode {
// Convert Windows path separators to Unix separators
path = filepath.ToSlash(path)

// If the path is a Windows path, the colon after the drive letter needs to be
// percent-encoded.
if parts := strings.Split(path, ":"); len(parts) > 1 {
path = parts[0] + "%3A" + parts[1]
}
}

return "file://" + "/" + path
}

// ToPath converts a URI to a file path from a format for a given client.
// Some clients represent URIs differently, and so this function exists to convert
// client URIs into a standard file paths.
func ToPath(client clients.Identifier, uri string) string {
path := strings.TrimPrefix(uri, "file://")

if client == clients.IdentifierVSCode {
if strings.Contains(path, ":") || strings.Contains(path, "%3A") {
path = strings.Replace(path, "%3A", ":", 1)
path = strings.TrimPrefix(path, "/")
}
}

return path
}
Loading

0 comments on commit ece9977

Please sign in to comment.