From b521a083baecec767602d37b1600be9c13a6ca72 Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Fri, 6 Mar 2026 14:01:32 -0600 Subject: [PATCH] dap: detect breakpoints for files when the case differs Case insensitive filesystems can cause breakpoints to not be seen or verified. This is particularly true on Windows where the drive letter can also participate in the filepath. Change the detection logic for a breakpoint to be case insensitive. At the same time, report the name of the source as part of the breakpoint so that the editor can be told which casing we're expecting to be used. Signed-off-by: Jonathan A. Sternberg --- dap/adapter.go | 32 +++++++++++++++++++++++++++----- dap/adapter_test.go | 4 ++-- tests/dap_build.go | 20 ++++++++++++++------ 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/dap/adapter.go b/dap/adapter.go index 7ced02dce..c9b823dbe 100644 --- a/dap/adapter.go +++ b/dap/adapter.go @@ -9,6 +9,7 @@ import ( "path" "path/filepath" "slices" + "strings" "sync" "sync/atomic" @@ -573,7 +574,9 @@ func (b *breakpointMap) Set(fname string, sbps []dap.SourceBreakpoint) (breakpoi // null back in the JSON if there are no breakpoints breakpoints = []dap.Breakpoint{} - prev := b.byPath[fname] + // Use lowercase paths to normalize the case. We can only know the correct casing after + // we intersect the breakpoint map. When we report the pending breakpoint to the editor, + prev := b.getByPath(fname) for _, sbp := range sbps { index := slices.IndexFunc(prev, func(e dap.Breakpoint) bool { return sbp.Line >= e.Line && sbp.Line <= e.EndLine && sbp.Column >= e.Column && sbp.Column <= e.EndColumn @@ -589,12 +592,16 @@ func (b *breakpointMap) Set(fname string, sbps []dap.SourceBreakpoint) (breakpoi EndLine: sbp.Line, Column: sbp.Column, EndColumn: sbp.Column, - Reason: "pending", + Source: &dap.Source{ + Name: path.Base(fname), + Path: fname, + }, + Reason: "pending", } } breakpoints = append(breakpoints, bp) } - b.byPath[fname] = breakpoints + b.setByPath(fname, breakpoints) return breakpoints } @@ -615,7 +622,7 @@ func (b *breakpointMap) Intersect(ctx Context, src *pb.Source, ws string) map[di for _, info := range src.Infos { fname := filepath.Join(ws, info.Filename) - bps := b.byPath[fname] + bps := b.getByPath(fname) for _, bp := range bps { if !bp.Verified && bp.Reason != "failed" { bp.Reason = "failed" @@ -656,7 +663,7 @@ func (b *breakpointMap) intersect(ctx Context, src *pb.Source, locs *pb.Location info := src.Infos[loc.SourceIndex] fname := filepath.Join(ws, info.Filename) - bps := b.byPath[fname] + bps := b.getByPath(fname) if len(bps) == 0 { // No breakpoints for this file. continue @@ -675,6 +682,13 @@ func (b *breakpointMap) intersect(ctx Context, src *pb.Source, locs *pb.Location bp.Verified = true bp.Reason = "" + // The path from the source might be different than the path + // the editor sent to us just because of different casing. + // Prefer the version given to us from buildkit and tell + // the editor what the proper casing for this should be. + bp.Source.Name = path.Base(fname) + bp.Source.Path = fname + ctx.C() <- &dap.BreakpointEvent{ Event: dap.Event{Event: "breakpoint"}, Body: dap.BreakpointEventBody{ @@ -689,3 +703,11 @@ func (b *breakpointMap) intersect(ctx Context, src *pb.Source, locs *pb.Location } return 0 } + +func (b *breakpointMap) setByPath(fname string, bps []dap.Breakpoint) { + b.byPath[strings.ToLower(fname)] = bps +} + +func (b *breakpointMap) getByPath(fname string) []dap.Breakpoint { + return b.byPath[strings.ToLower(fname)] +} diff --git a/dap/adapter_test.go b/dap/adapter_test.go index cf2f1db15..21760ccb5 100644 --- a/dap/adapter_test.go +++ b/dap/adapter_test.go @@ -202,7 +202,7 @@ func TestBreakpointMapIntersectVerified(t *testing.T) { assert.Len(t, digests, wantMatches) expectedEvents := make(map[int]struct{}) - for i, bp := range bm.byPath[fpath] { + for i, bp := range bm.getByPath(fpath) { if breakpointCases[i].expectVerified { expectedEvents[bp.Id] = struct{}{} } @@ -226,7 +226,7 @@ func TestBreakpointMapIntersectVerified(t *testing.T) { } } - stored := bm.byPath[fpath] + stored := bm.getByPath(fpath) if assert.Len(t, stored, len(breakpointCases)) { for i, bc := range breakpointCases { assert.Equal(t, bc.expectVerified, stored[i].Verified, "breakpoint %d (%s) mismatch", i, bc.desc) diff --git a/tests/dap_build.go b/tests/dap_build.go index ce7f169b6..32816f150 100644 --- a/tests/dap_build.go +++ b/tests/dap_build.go @@ -228,18 +228,26 @@ func testDapBuildVerifiedBreakpoints(t *testing.T, sb integration.Sandbox) { { Reason: "changed", Breakpoint: dap.Breakpoint{ - Id: 1, - Line: 2, - EndLine: 2, + Id: 1, + Line: 2, + EndLine: 2, + Source: &dap.Source{ + Name: "Dockerfile", + Path: path.Join(dir, "Dockerfile"), + }, Verified: true, }, }, { Reason: "changed", Breakpoint: dap.Breakpoint{ - Id: 2, - Line: 10, - EndLine: 10, + Id: 2, + Line: 10, + EndLine: 10, + Source: &dap.Source{ + Name: "Dockerfile", + Path: path.Join(dir, "Dockerfile"), + }, Verified: false, Reason: "failed", },