Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion internal/lspserver/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,14 @@ func (s *Server) handleDiagnostic(ctx context.Context, params *protocol.Document
}

// Document not open — read from disk.
// Untitled documents have no backing file; return empty diagnostics.
if isVirtualURI(uri) {
return &protocol.DocumentDiagnosticResponse{
FullDocumentDiagnosticReport: &protocol.RelatedFullDocumentDiagnosticReport{
Items: []*protocol.Diagnostic{},
},
}, nil
}
filePath := uriToPath(uri)
return s.pullDiagnosticsFromDisk(ctx, uri, filePath, params.PreviousResultId)
}
Expand Down Expand Up @@ -527,12 +535,46 @@ func clampUint32(v int) uint32 {
return uint32(v) //nolint:gosec // line/column numbers are well within uint32 range
}

// uriToPath converts a file:// URI to a local file path.
// isVirtualURI reports whether docURI refers to a virtual document that doesn't
// have a backing file on disk (e.g. untitled:, vscode-notebook-cell:).
func isVirtualURI(docURI string) bool {
// Fast path for the most common case.
if strings.HasPrefix(docURI, "file:") {
return false
}
parsed, err := url.Parse(docURI)
if err != nil {
// If parsing fails, it's unlikely to be a URI with a scheme.
// Treat as a file path.
return false
}
return parsed.Scheme != "" && parsed.Scheme != "file"
}

// uriToPath converts a document URI to a local file path for linting purposes.
//
// For file:// URIs this returns the real filesystem path.
// For non-file URIs (e.g. untitled://) this returns a synthetic path anchored
// at the working directory so that config discovery finds the project-level
// .tally.toml. The synthetic name is always "Dockerfile" because tally only
// lints Dockerfiles.
func uriToPath(docURI string) string {
parsed, err := url.Parse(docURI)
if err != nil {
return strings.TrimPrefix(docURI, "file://")
}

// Non-file URIs (e.g. untitled://) represent unsaved documents with no
// on-disk path. Return a synthetic path so config discovery and settings
// matching work relative to the project root.
if parsed.Scheme != "" && parsed.Scheme != "file" {
wd, err := os.Getwd()
if err != nil {
return "Dockerfile"
}
return filepath.Join(wd, "Dockerfile")
}

path := parsed.Path
if runtime.GOOS == "windows" {
// UNC paths: file://server/share/path → \\server\share\path
Expand Down
53 changes: 53 additions & 0 deletions internal/lspserver/diagnostics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

protocol "github.com/wharflab/tally/internal/lsp/protocol"
)

func TestScheduleFullPass_ReplacesExistingTimer(t *testing.T) {
Expand Down Expand Up @@ -68,6 +70,57 @@ func TestCancelAllShellcheckDebounce_ClearsPendingTimers(t *testing.T) {
assert.Empty(t, s.shellcheckDebounce)
}

func TestPublishDiagnostics_UntitledURI(t *testing.T) {
t.Parallel()

s := New()
uri := "untitled:Untitled-1"

var receivedURI string
var receivedContent []byte
done := make(chan struct{})

s.diagnosticsRunFn = func(_ context.Context, docURI string, _ int32, content []byte) {
receivedURI = docURI
receivedContent = append([]byte(nil), content...)
close(done)
}

s.publishDiagnostics(context.Background(), &Document{
URI: uri,
LanguageID: "dockerfile",
Version: 1,
Content: "FROM alpine\nRUN apt-get update\n",
})

select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for diagnostics")
}

assert.Equal(t, uri, receivedURI)
assert.Equal(t, "FROM alpine\nRUN apt-get update\n", string(receivedContent))
}

func TestHandleDiagnostic_UntitledURI_ClosedDocument(t *testing.T) {
t.Parallel()

s := New()
// Don't open the document — simulate a pull diagnostic request for a closed untitled doc.
result, err := s.handleDiagnostic(context.Background(), &protocol.DocumentDiagnosticParams{
TextDocument: protocol.TextDocumentIdentifier{
Uri: "untitled:Untitled-1",
},
})
require.NoError(t, err)

resp, ok := result.(*protocol.DocumentDiagnosticResponse)
require.True(t, ok)
require.NotNil(t, resp.FullDocumentDiagnosticReport)
assert.Empty(t, resp.FullDocumentDiagnosticReport.Items)
}

func TestPublishDiagnostics_CoalescesPerURI(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 4 additions & 0 deletions internal/lspserver/execute_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,9 @@ func (s *Server) contentForURI(uri string) ([]byte, error) {
if doc := s.documents.Get(uri); doc != nil {
return []byte(doc.Content), nil
}
// Untitled documents have no backing file on disk.
if isVirtualURI(uri) {
return nil, os.ErrNotExist
}
return os.ReadFile(uriToPath(uri))
}
21 changes: 21 additions & 0 deletions internal/lspserver/execute_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,27 @@ func TestContentForURI_ReturnsOpenDocumentContent(t *testing.T) {
assert.Equal(t, "FROM alpine:3.18\n", string(content))
}

func TestContentForURI_ReturnsOpenUntitledDocumentContent(t *testing.T) {
t.Parallel()

s := New()
uri := "untitled:Untitled-1"
s.documents.Open(uri, "dockerfile", 1, "FROM alpine:3.18\n")

content, err := s.contentForURI(uri)
require.NoError(t, err)
assert.Equal(t, "FROM alpine:3.18\n", string(content))
}

func TestContentForURI_UntitledURI_ClosedDocument(t *testing.T) {
t.Parallel()

s := New()
// Don't open the document — untitled URIs have no backing file.
_, err := s.contentForURI("untitled:Untitled-1")
require.ErrorIs(t, err, os.ErrNotExist)
}

func TestContentForURI_ReadsFromDiskWhenNotOpen(t *testing.T) {
t.Parallel()

Expand Down
44 changes: 42 additions & 2 deletions internal/lspserver/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,48 @@ func TestSeverityConversion(t *testing.T) {

func TestURIToPath(t *testing.T) {
t.Parallel()
path := uriToPath("file:///tmp/Dockerfile")
assert.Equal(t, filepath.FromSlash("/tmp/Dockerfile"), path)

t.Run("file URI", func(t *testing.T) {
t.Parallel()
path := uriToPath("file:///tmp/Dockerfile")
assert.Equal(t, filepath.FromSlash("/tmp/Dockerfile"), path)
})

t.Run("untitled URI returns synthetic path", func(t *testing.T) {
t.Parallel()
path := uriToPath("untitled:Untitled-1")
assert.True(t, filepath.IsAbs(path), "untitled URI should resolve to an absolute path")
assert.Equal(t, "Dockerfile", filepath.Base(path))
})

t.Run("vscode-notebook URI returns synthetic path", func(t *testing.T) {
t.Parallel()
path := uriToPath("vscode-notebook-cell://authority/path")
assert.True(t, filepath.IsAbs(path))
assert.Equal(t, "Dockerfile", filepath.Base(path))
})
}

func TestIsVirtualURI(t *testing.T) {
t.Parallel()

tests := []struct {
uri string
want bool
}{
{"untitled:Untitled-1", true},
{"untitled://Untitled-1", true},
{"vscode-notebook-cell://authority/path", true},
{"file:///tmp/Dockerfile", false},
{"/tmp/Dockerfile", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.uri, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.want, isVirtualURI(tt.uri), "isVirtualURI(%q)", tt.uri)
})
}
}

func TestCancelPreempter_HandlesCancelRequest(t *testing.T) {
Expand Down
Loading