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
6 changes: 4 additions & 2 deletions desktop/internal/core/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,8 @@ type buildOutcome struct {
Version *BuildToolVersion `json:"version"`
// Dependencies is the answer of build.decodeDependencies.
Dependencies *BuildDependencyTree `json:"dependencies"`
// Storage and Edit answer build.resolveStoredOptions and build.editStoredOption.
// Storage and Edit answer build.resolveStoredOptions and build.editStoredOption; build.editIgnoredPaths
// decodes its own edit.
Storage *BuildStoredOptions `json:"storage"`
Edit *BuildOptionEdit `json:"edit"`
// Discovery answers build.decodeTestDiscovery.
Expand Down Expand Up @@ -312,7 +313,8 @@ func (c *Client) DetectBuildSystemsWithOptions(ctx context.Context, workspaceRoo
InstanceOptions map[string]map[string]map[string]json.RawMessage `json:"instanceOptions,omitempty"`
InstanceDefaults map[string]map[string]json.RawMessage `json:"instanceDefaults,omitempty"`
EmptySelections map[string]map[string][]string `json:"emptySelections,omitempty"`
}{workspaceRoot, plugins, options.Options, options.InstanceOptions, options.InstanceDefaults, options.EmptySelections}, &result); err != nil {
IgnoredPaths []string `json:"ignoredPaths"`
}{workspaceRoot, plugins, options.Options, options.InstanceOptions, options.InstanceDefaults, options.EmptySelections, options.IgnoredPaths}, &result); err != nil {
return BuildDetection{}, err
}
if err := result.failure(command); err != nil {
Expand Down
72 changes: 70 additions & 2 deletions desktop/internal/core/build_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,56 @@ import (

// BuildDetectOptions are the stored option inputs of build.detect: values by build system and option ID,
// instance values by root, instance defaults and the explicit empty selections. Core merges them into each
// instance's options and names the rejected ones in droppedOptions.
// instance's options and names the rejected ones in droppedOptions. IgnoredPaths are the directories the
// scan does not enter; nil is Core's default, every directory whose name starts with a dot, and an empty
// list ignores nothing beyond the shared scan policy.
type BuildDetectOptions struct {
Options map[string]map[string]json.RawMessage `json:"options,omitempty"`
InstanceOptions map[string]map[string]map[string]json.RawMessage `json:"instanceOptions,omitempty"`
InstanceDefaults map[string]map[string]json.RawMessage `json:"instanceDefaults,omitempty"`
EmptySelections map[string]map[string][]string `json:"emptySelections,omitempty"`
IgnoredPaths []string `json:"ignoredPaths"`
}

// BuildStoredOptions is the checked merge of the team document and the platform-local document that
// build.resolveStoredOptions returns. Document is the merged document, Local the validated local one.
// build.resolveStoredOptions returns. Document is the merged document, Local the validated local one, and
// IgnoredPaths the list build detection uses with the layer it came from.
type BuildStoredOptions struct {
Document json.RawMessage `json:"document"`
Local json.RawMessage `json:"local"`
EmptySelections map[string]map[string][]string `json:"emptySelections"`
IgnoredPaths BuildIgnoredPaths `json:"ignoredPaths"`
}

// The layers an ignored-path list comes from: the local document, the team document, or neither, when
// Core's default applies.
const (
BuildIgnoredPathsLocal = "local"
BuildIgnoredPathsTeam = "team"
BuildIgnoredPathsDefault = "default"
)

// BuildIgnoredPaths are the .gitignore patterns, relative to the workspace root, of the directories build
// detection does not enter, and the layer that set them: a local list replaces the team's whole.
type BuildIgnoredPaths struct {
Patterns []string `json:"patterns"`
Source string `json:"source"`
}

// BuildIgnoredPathsEditRequest replaces the local ignored-path list with Patterns, an empty list being the
// explicit choice to ignore nothing, or removes it when Inherit is set, so the team list or Core's default
// applies again.
type BuildIgnoredPathsEditRequest struct {
Document json.RawMessage
Local json.RawMessage
Patterns []string
Inherit bool
}

// BuildIgnoredPathsEdit is the local document after an ignored-path edit and the list that applies with it.
type BuildIgnoredPathsEdit struct {
Local json.RawMessage `json:"local"`
IgnoredPaths BuildIgnoredPaths `json:"ignoredPaths"`
}

// BuildOptionEditRequest changes one declared local preference of an instance. A nil Value removes the local
Expand Down Expand Up @@ -92,3 +128,35 @@ func (c *Client) EditStoredBuildOption(ctx context.Context, manifest json.RawMes
}
return *result.Edit, nil
}

// EditBuildIgnoredPaths applies an ignored-path edit to the local document. It reads and writes no file: the
// team document is only read, and the host writes edit.Local itself, atomically, and reads it back. Core
// refuses a list it cannot use with plugin.build.invalidIgnoredPath at ignoredPaths[<index>], the first
// pattern not accepted, or at ignoredPaths when the patterns cannot be used together.
func (c *Client) EditBuildIgnoredPaths(ctx context.Context, request BuildIgnoredPathsEditRequest) (BuildIgnoredPathsEdit, error) {
const command = "build.editIgnoredPaths"
// patterns is null to remove the local list, and a list, empty or not, to replace it.
var patterns any
if !request.Inherit {
patterns = append([]string{}, request.Patterns...)
}
// Both commands answer under edit; this one with the ignored paths in place of the options.
var result struct {
buildOutcome
Edit *BuildIgnoredPathsEdit `json:"edit"`
}
if err := c.Execute(ctx, command, struct {
Document json.RawMessage `json:"document"`
Local json.RawMessage `json:"local"`
Patterns any `json:"patterns"`
}{request.Document, request.Local, patterns}, &result); err != nil {
return BuildIgnoredPathsEdit{}, err
}
if err := result.failure(command); err != nil {
return BuildIgnoredPathsEdit{}, err
}
if result.Edit == nil {
return BuildIgnoredPathsEdit{}, newParseError(command, "build.editIgnoredPaths returned no edit", "")
}
return *result.Edit, nil
}
113 changes: 113 additions & 0 deletions desktop/internal/core/build_options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package core

import (
"encoding/json"
"errors"
"os"
"path/filepath"
"slices"
"testing"

"github.com/1lck/Lithe-IDEA/desktop/internal/contractfixture"
)

// emptyBuildLocal is the local Build document of a project that stores nothing.
var emptyBuildLocal = json.RawMessage(`{"version":1,"document":{"version":1,"systems":{}}}`)

// The ignored paths come from the local document, else the team document, else Core's default of every
// dot-named directory. An edit replaces the local list, an empty list included, or removes it so the
// team's applies again; Core names the first pattern it does not accept.
func TestBuildIgnoredPathsResolveFromTheLayersAndEditTheLocalList(t *testing.T) {
client, ctx := NewClient(), testContext(t)
noTeam := json.RawMessage(`{"version":1,"systems":{}}`)
team := json.RawMessage(`{"version":1,"systems":{},"ignoredPaths":["vendor/"]}`)
resolved := func(document, local json.RawMessage) BuildIgnoredPaths {
t.Helper()
storage, err := client.ResolveStoredBuildOptions(ctx, document, local)
if err != nil {
t.Fatal(err)
}
return storage.IgnoredPaths
}
if got := resolved(noTeam, emptyBuildLocal); !slices.Equal(got.Patterns, []string{".*"}) || got.Source != BuildIgnoredPathsDefault {
t.Fatalf("without a stored list the ignored paths are %+v, want Core's default", got)
}
if got := resolved(team, emptyBuildLocal); !slices.Equal(got.Patterns, []string{"vendor/"}) || got.Source != BuildIgnoredPathsTeam {
t.Fatalf("with the team's list the ignored paths are %+v", got)
}

edit, err := client.EditBuildIgnoredPaths(ctx, BuildIgnoredPathsEditRequest{Document: team, Local: emptyBuildLocal, Patterns: []string{".*", "!.config"}})
if err != nil {
t.Fatal(err)
}
if got := edit.IgnoredPaths; !slices.Equal(got.Patterns, []string{".*", "!.config"}) || got.Source != BuildIgnoredPathsLocal {
t.Fatalf("after the edit the ignored paths are %+v", got)
}
if got := resolved(team, edit.Local); !slices.Equal(got.Patterns, []string{".*", "!.config"}) || got.Source != BuildIgnoredPathsLocal {
t.Fatalf("the edited local document resolves to %+v", got)
}

// No patterns is the choice to ignore nothing, which is kept, not the default.
none, err := client.EditBuildIgnoredPaths(ctx, BuildIgnoredPathsEditRequest{Document: team, Local: edit.Local})
if err != nil {
t.Fatal(err)
}
if got := resolved(team, none.Local); got.Patterns == nil || len(got.Patterns) != 0 || got.Source != BuildIgnoredPathsLocal {
t.Fatalf("an empty local list resolves to %+v, want an empty list from the local document", got)
}

inherited, err := client.EditBuildIgnoredPaths(ctx, BuildIgnoredPathsEditRequest{Document: team, Local: none.Local, Inherit: true})
if err != nil {
t.Fatal(err)
}
if got := inherited.IgnoredPaths; !slices.Equal(got.Patterns, []string{"vendor/"}) || got.Source != BuildIgnoredPathsTeam {
t.Fatalf("removing the local list leaves %+v, want the team's", got)
}

_, err = client.EditBuildIgnoredPaths(ctx, BuildIgnoredPathsEditRequest{Document: team, Local: emptyBuildLocal, Patterns: []string{".*", "# note"}})
var rejection *MarketplaceRejection
if !errors.As(err, &rejection) || rejection.Code != "plugin.build.invalidIgnoredPath" || rejection.Field != "ignoredPaths[1]" {
t.Fatalf("a comment line answered %v, want plugin.build.invalidIgnoredPath at ignoredPaths[1]", err)
}
}

// Detection skips every dot-named directory unless it is given the list to use; an empty list ignores
// nothing beyond the shared scan policy.
func TestBuildDetectSkipsTheIgnoredPaths(t *testing.T) {
client, ctx := NewClient(), testContext(t)
root := t.TempDir()
for _, dir := range []string{".", ".claude/copy", "tools"} {
if err := os.MkdirAll(filepath.Join(root, dir), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, dir, "Cargo.toml"), []byte("[package]\nname = \"app\"\nversion = \"0.1.0\"\n"), 0o600); err != nil {
t.Fatal(err)
}
}
manifest, err := os.ReadFile(contractfixture.Path("fixtures/build-systems/build-static-file.plugin.json"))
if err != nil {
t.Fatal(err)
}
roots := func(ignored []string) []string {
t.Helper()
detection, err := client.DetectBuildSystemsWithOptions(ctx, root, []json.RawMessage{manifest}, BuildDetectOptions{IgnoredPaths: ignored})
if err != nil {
t.Fatal(err)
}
var roots []string
for _, instance := range detection.Instances {
roots = append(roots, instance.Root)
}
slices.Sort(roots)
return roots
}
if got := roots(nil); !slices.Equal(got, []string{".", "tools"}) {
t.Fatalf("by default detection finds %q, want the dot-named directory skipped", got)
}
if got := roots([]string{}); !slices.Equal(got, []string{".", ".claude/copy", "tools"}) {
t.Fatalf("ignoring nothing detection finds %q", got)
}
if got := roots([]string{"/tools"}); !slices.Equal(got, []string{".", ".claude/copy"}) {
t.Fatalf("ignoring /tools detection finds %q", got)
}
}
18 changes: 17 additions & 1 deletion desktop/internal/i18n/shared.generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions desktop/internal/workbench/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type Kernel interface {
DetectBuildSystemsWithOptions(ctx context.Context, workspaceRoot string, manifests []json.RawMessage, options core.BuildDetectOptions) (core.BuildDetection, error)
ResolveStoredBuildOptions(ctx context.Context, document, local json.RawMessage) (core.BuildStoredOptions, error)
EditStoredBuildOption(ctx context.Context, manifest json.RawMessage, buildSystemID string, request core.BuildOptionEditRequest) (core.BuildOptionEdit, error)
EditBuildIgnoredPaths(ctx context.Context, request core.BuildIgnoredPathsEditRequest) (core.BuildIgnoredPathsEdit, error)
IDEHostOpen(ctx context.Context, request core.IDEHostOpenRequest) (core.IDEHostOpenResult, error)
IDEHostPoll(ctx context.Context, hostID string) (core.IDEHostPollResult, error)
IDEHostClaim(ctx context.Context, hostID, requestID string) (core.IDEHostClaimResult, error)
Expand Down
5 changes: 3 additions & 2 deletions desktop/internal/workbench/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ type buildState struct {
loaded bool
loading bool
failed string
// options is the Build options editor of the selected instance (build_options.go).
options buildOptionsEditor
// options is the Build options dialog (build_options.go) and optionsSessions counts its openings.
options buildOptionsEditor
optionsSessions int
// reloadPending says a stored option changed while a detection was running, so the instances are
// detected again when that detection ends.
reloadPending bool
Expand Down
Loading
Loading