diff --git a/src/segments/project.go b/src/segments/project.go index 2e177aae..0293318a 100644 --- a/src/segments/project.go +++ b/src/segments/project.go @@ -3,22 +3,33 @@ package segments import ( "encoding/json" "encoding/xml" + "errors" "oh-my-posh/environment" "oh-my-posh/properties" + "oh-my-posh/regex" "path/filepath" + "strings" "github.com/BurntSushi/toml" ) type ProjectItem struct { Name string - File string - Fetcher func(item ProjectItem) (string, string) + Files []string + Fetcher func(item ProjectItem) *ProjectData } type ProjectData struct { Version string Name string + Target string +} + +func (p *ProjectData) enabled() bool { + if p == nil { + return false + } + return len(p.Version) > 0 || len(p.Name) > 0 || len(p.Target) > 0 } // Rust Cargo package @@ -56,15 +67,19 @@ type Project struct { func (n *Project) Enabled() bool { for _, item := range n.projects { if n.hasProjectFile(item) { - n.Version, n.Name = item.Fetcher(*item) - return len(n.Version) > 0 || len(n.Name) > 0 + data := item.Fetcher(*item) + if data == nil { + continue + } + n.ProjectData = *data + return n.enabled() } } return false } func (n *Project) Template() string { - return " {{ if .Error }}{{ .Error }}{{ else }}{{ if .Version }}\uf487 {{.Version}}{{ end }} {{ if .Name }}{{ .Name }}{{ end }}{{ end }} " + return " {{ if .Error }}{{ .Error }}{{ else }}{{ if .Version }}\uf487 {{.Version}} {{ end }}{{ if .Name }}{{ .Name }} {{ end }}{{ if .Target }}\uf9fd {{.Target}} {{ end }}{{ end }}" //nolint:lll } func (n *Project) Init(props properties.Properties, env environment.Environment) { @@ -74,76 +89,92 @@ func (n *Project) Init(props properties.Properties, env environment.Environment) n.projects = []*ProjectItem{ { Name: "node", - File: "package.json", + Files: []string{"package.json"}, Fetcher: n.getNodePackage, }, { Name: "cargo", - File: "Cargo.toml", + Files: []string{"Cargo.toml"}, Fetcher: n.getCargoPackage, }, { Name: "poetry", - File: "pyproject.toml", + Files: []string{"pyproject.toml"}, Fetcher: n.getPoetryPackage, }, { Name: "php", - File: "composer.json", + Files: []string{"composer.json"}, Fetcher: n.getNodePackage, }, { Name: "nuspec", - File: "*.nuspec", + Files: []string{"*.nuspec"}, Fetcher: n.getNuSpecPackage, }, + { + Name: "dotnet", + Files: []string{"*.vbproj", "*.fsproj", "*.csproj"}, + Fetcher: n.getDotnetProject, + }, } } func (n *Project) hasProjectFile(p *ProjectItem) bool { - return n.env.HasFiles(p.File) + for _, file := range p.Files { + if n.env.HasFiles(file) { + return true + } + } + return false } -func (n *Project) getNodePackage(item ProjectItem) (string, string) { - content := n.env.FileContent(item.File) +func (n *Project) getNodePackage(item ProjectItem) *ProjectData { + content := n.env.FileContent(item.Files[0]) var data ProjectData err := json.Unmarshal([]byte(content), &data) if err != nil { n.Error = err.Error() - return "", "" + return nil } - return data.Version, data.Name + return &data } -func (n *Project) getCargoPackage(item ProjectItem) (string, string) { - content := n.env.FileContent(item.File) +func (n *Project) getCargoPackage(item ProjectItem) *ProjectData { + content := n.env.FileContent(item.Files[0]) var data CargoTOML _, err := toml.Decode(content, &data) if err != nil { n.Error = err.Error() - return "", "" + return nil } - return data.Package.Version, data.Package.Name + return &ProjectData{ + Version: data.Package.Version, + Name: data.Package.Name, + } } -func (n *Project) getPoetryPackage(item ProjectItem) (string, string) { - content := n.env.FileContent(item.File) +func (n *Project) getPoetryPackage(item ProjectItem) *ProjectData { + content := n.env.FileContent(item.Files[0]) var data PyProjectTOML _, err := toml.Decode(content, &data) if err != nil { n.Error = err.Error() - return "", "" + return nil } - return data.Tool.Poetry.Version, data.Tool.Poetry.Name + return &ProjectData{ + Version: data.Tool.Poetry.Version, + Name: data.Tool.Poetry.Name, + } } -func (n *Project) getNuSpecPackage(item ProjectItem) (string, string) { +func (n *Project) getNuSpecPackage(item ProjectItem) *ProjectData { files := n.env.LsDir(n.env.Pwd()) var content string // get the first match only @@ -158,8 +189,40 @@ func (n *Project) getNuSpecPackage(item ProjectItem) (string, string) { err := xml.Unmarshal([]byte(content), &data) if err != nil { n.Error = err.Error() - return "", "" + return nil } - return data.MetaData.Version, data.MetaData.Title + return &ProjectData{ + Version: data.MetaData.Version, + Name: data.MetaData.Title, + } +} + +func (n *Project) getDotnetProject(item ProjectItem) *ProjectData { + files := n.env.LsDir(n.env.Pwd()) + var name string + var content string + // get the first match only + for _, file := range files { + extension := filepath.Ext(file.Name()) + if extension == ".csproj" || extension == ".fsproj" || extension == ".vbproj" { + name = strings.TrimSuffix(file.Name(), filepath.Ext(file.Name())) + content = n.env.FileContent(file.Name()) + break + } + } + // the name of the parameter may differ depending on the version, + // so instead of xml.Unmarshal() we use regex: + tag := "(?P<.*TargetFramework.*>(?P.*))" + values := regex.FindNamedRegexMatch(tag, content) + if len(values) == 0 { + n.Error = errors.New("cannot extract TFM from " + name + " project file").Error() + return nil + } + target := values["TFM"] + + return &ProjectData{ + Target: target, + Name: name, + } } diff --git a/src/segments/project_test.go b/src/segments/project_test.go index c07410a8..94fe4e6e 100644 --- a/src/segments/project_test.go +++ b/src/segments/project_test.go @@ -5,6 +5,7 @@ import ( "oh-my-posh/mock" "oh-my-posh/properties" "os" + "path/filepath" "testing" "github.com/alecthomas/assert" @@ -12,6 +13,10 @@ import ( testify_mock "github.com/stretchr/testify/mock" ) +const ( + hasFiles = "HasFiles" +) + type MockDirEntry struct { name string isDir bool @@ -185,9 +190,9 @@ func TestPackage(t *testing.T) { for _, tc := range cases { env := new(mock.MockedEnvironment) - env.On("HasFiles", testify_mock.Anything).Run(func(args testify_mock.Arguments) { + env.On(hasFiles, testify_mock.Anything).Run(func(args testify_mock.Arguments) { for _, c := range env.ExpectedCalls { - if c.Method == "HasFiles" { + if c.Method == hasFiles { c.ReturnArguments = testify_mock.Arguments{args.Get(0).(string) == tc.File} } } @@ -238,9 +243,9 @@ func TestNuspecPackage(t *testing.T) { for _, tc := range cases { env := new(mock.MockedEnvironment) - env.On("HasFiles", testify_mock.Anything).Run(func(args testify_mock.Arguments) { + env.On(hasFiles, testify_mock.Anything).Run(func(args testify_mock.Arguments) { for _, c := range env.ExpectedCalls { - if c.Method != "HasFiles" { + if c.Method != hasFiles { continue } if args.Get(0).(string) == "*.nuspec" { @@ -266,3 +271,76 @@ func TestNuspecPackage(t *testing.T) { } } } + +func TestDotnetProject(t *testing.T) { + cases := []struct { + Case string + FileName string + HasFiles bool + ProjectContents string + ExpectedString string + ExpectedEnabled bool + }{ + { + Case: "valid .csproj file", + FileName: "Valid.csproj", + HasFiles: true, + ProjectContents: "...net7.0...", + ExpectedEnabled: true, + ExpectedString: "Valid \uf9fd net7.0", + }, + { + Case: "valid .fsproj file", + FileName: "Valid.fsproj", + HasFiles: true, + ProjectContents: "...net6.0...", + ExpectedEnabled: true, + ExpectedString: "Valid \uf9fd net6.0", + }, + { + Case: "valid .vbproj file", + FileName: "Valid.vbproj", + HasFiles: true, + ProjectContents: "...net5.0...", + ExpectedEnabled: true, + ExpectedString: "Valid \uf9fd net5.0", + }, + { + Case: "invalid or empty contents", + FileName: "Invalid.csproj", + HasFiles: true, + ExpectedEnabled: false, + ExpectedString: "cannot extract TFM from Invalid project file", + }, + { + Case: "no files", + HasFiles: false, + ExpectedEnabled: false, + }, + } + + for _, tc := range cases { + env := new(mock.MockedEnvironment) + env.On(hasFiles, testify_mock.Anything).Run(func(args testify_mock.Arguments) { + for _, c := range env.ExpectedCalls { + if c.Method == hasFiles { + pattern := "*" + filepath.Ext(tc.FileName) + c.ReturnArguments = testify_mock.Arguments{args.Get(0).(string) == pattern} + } + } + }) + env.On("Pwd").Return("posh") + env.On("LsDir", "posh").Return([]fs.DirEntry{ + &MockDirEntry{ + name: tc.FileName, + }, + }) + env.On("FileContent", tc.FileName).Return(tc.ProjectContents) + pkg := &Project{} + pkg.Init(properties.Map{}, env) + assert.Equal(t, tc.ExpectedEnabled, pkg.Enabled(), tc.Case) + if tc.ExpectedEnabled { + assert.Equal(t, tc.ExpectedString, renderTemplate(env, pkg.Template(), pkg), tc.Case) + } + } +} diff --git a/website/docs/segments/project.mdx b/website/docs/segments/project.mdx index 6edcdd50..4116a3d9 100644 --- a/website/docs/segments/project.mdx +++ b/website/docs/segments/project.mdx @@ -15,6 +15,7 @@ Supports: - Poetry project (`pyproject.toml`) - PHP project (`composer.json`) - Any nuspec based project (`*.nuspec`, first file match info is displayed) +- .NET project (`*.csproj`, `*.vbproj` or `*.fsproj`, first file match info is displayed) ## Sample Configuration @@ -41,9 +42,10 @@ Supports: ### Properties -| Name | Type | Description | -| ---------- | -------- | --------------------------- | -| `.Version` | `string` | The version of your project | -| `.Name` | `string` | The name of your project | +| Name | Type | Description | +| ---------- | -------- | ---------------------------------------------------- | +| `.Version` | `string` | The version of your project | +| `.Target` | `string` | The target framwork/language version of your project | +| `.Name` | `string` | The name of your project | [templates]: /docs/configuration/templates