feat(project): enhance project segment with .NET

This commit is contained in:
Oleksandr Babieiev 2022-10-26 14:15:03 +02:00 committed by Jan De Dobbeleer
parent 69822e5c63
commit 7ae14646d7
3 changed files with 177 additions and 34 deletions

View file

@ -3,22 +3,33 @@ package segments
import ( import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"errors"
"oh-my-posh/environment" "oh-my-posh/environment"
"oh-my-posh/properties" "oh-my-posh/properties"
"oh-my-posh/regex"
"path/filepath" "path/filepath"
"strings"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
type ProjectItem struct { type ProjectItem struct {
Name string Name string
File string Files []string
Fetcher func(item ProjectItem) (string, string) Fetcher func(item ProjectItem) *ProjectData
} }
type ProjectData struct { type ProjectData struct {
Version string Version string
Name 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 // Rust Cargo package
@ -56,15 +67,19 @@ type Project struct {
func (n *Project) Enabled() bool { func (n *Project) Enabled() bool {
for _, item := range n.projects { for _, item := range n.projects {
if n.hasProjectFile(item) { if n.hasProjectFile(item) {
n.Version, n.Name = item.Fetcher(*item) data := item.Fetcher(*item)
return len(n.Version) > 0 || len(n.Name) > 0 if data == nil {
continue
}
n.ProjectData = *data
return n.enabled()
} }
} }
return false return false
} }
func (n *Project) Template() string { 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) { 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{ n.projects = []*ProjectItem{
{ {
Name: "node", Name: "node",
File: "package.json", Files: []string{"package.json"},
Fetcher: n.getNodePackage, Fetcher: n.getNodePackage,
}, },
{ {
Name: "cargo", Name: "cargo",
File: "Cargo.toml", Files: []string{"Cargo.toml"},
Fetcher: n.getCargoPackage, Fetcher: n.getCargoPackage,
}, },
{ {
Name: "poetry", Name: "poetry",
File: "pyproject.toml", Files: []string{"pyproject.toml"},
Fetcher: n.getPoetryPackage, Fetcher: n.getPoetryPackage,
}, },
{ {
Name: "php", Name: "php",
File: "composer.json", Files: []string{"composer.json"},
Fetcher: n.getNodePackage, Fetcher: n.getNodePackage,
}, },
{ {
Name: "nuspec", Name: "nuspec",
File: "*.nuspec", Files: []string{"*.nuspec"},
Fetcher: n.getNuSpecPackage, Fetcher: n.getNuSpecPackage,
}, },
{
Name: "dotnet",
Files: []string{"*.vbproj", "*.fsproj", "*.csproj"},
Fetcher: n.getDotnetProject,
},
} }
} }
func (n *Project) hasProjectFile(p *ProjectItem) bool { 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) { func (n *Project) getNodePackage(item ProjectItem) *ProjectData {
content := n.env.FileContent(item.File) content := n.env.FileContent(item.Files[0])
var data ProjectData var data ProjectData
err := json.Unmarshal([]byte(content), &data) err := json.Unmarshal([]byte(content), &data)
if err != nil { if err != nil {
n.Error = err.Error() n.Error = err.Error()
return "", "" return nil
} }
return data.Version, data.Name return &data
} }
func (n *Project) getCargoPackage(item ProjectItem) (string, string) { func (n *Project) getCargoPackage(item ProjectItem) *ProjectData {
content := n.env.FileContent(item.File) content := n.env.FileContent(item.Files[0])
var data CargoTOML var data CargoTOML
_, err := toml.Decode(content, &data) _, err := toml.Decode(content, &data)
if err != nil { if err != nil {
n.Error = err.Error() 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) { func (n *Project) getPoetryPackage(item ProjectItem) *ProjectData {
content := n.env.FileContent(item.File) content := n.env.FileContent(item.Files[0])
var data PyProjectTOML var data PyProjectTOML
_, err := toml.Decode(content, &data) _, err := toml.Decode(content, &data)
if err != nil { if err != nil {
n.Error = err.Error() 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()) files := n.env.LsDir(n.env.Pwd())
var content string var content string
// get the first match only // get the first match only
@ -158,8 +189,40 @@ func (n *Project) getNuSpecPackage(item ProjectItem) (string, string) {
err := xml.Unmarshal([]byte(content), &data) err := xml.Unmarshal([]byte(content), &data)
if err != nil { if err != nil {
n.Error = err.Error() 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<TAG><.*TargetFramework.*>(?P<TFM>.*)</.*TargetFramework.*>)"
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,
}
} }

View file

@ -5,6 +5,7 @@ import (
"oh-my-posh/mock" "oh-my-posh/mock"
"oh-my-posh/properties" "oh-my-posh/properties"
"os" "os"
"path/filepath"
"testing" "testing"
"github.com/alecthomas/assert" "github.com/alecthomas/assert"
@ -12,6 +13,10 @@ import (
testify_mock "github.com/stretchr/testify/mock" testify_mock "github.com/stretchr/testify/mock"
) )
const (
hasFiles = "HasFiles"
)
type MockDirEntry struct { type MockDirEntry struct {
name string name string
isDir bool isDir bool
@ -185,9 +190,9 @@ func TestPackage(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
env := new(mock.MockedEnvironment) 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 { for _, c := range env.ExpectedCalls {
if c.Method == "HasFiles" { if c.Method == hasFiles {
c.ReturnArguments = testify_mock.Arguments{args.Get(0).(string) == tc.File} c.ReturnArguments = testify_mock.Arguments{args.Get(0).(string) == tc.File}
} }
} }
@ -238,9 +243,9 @@ func TestNuspecPackage(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
env := new(mock.MockedEnvironment) 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 { for _, c := range env.ExpectedCalls {
if c.Method != "HasFiles" { if c.Method != hasFiles {
continue continue
} }
if args.Get(0).(string) == "*.nuspec" { 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: "...<TargetFramework>net7.0</TargetFramework>...",
ExpectedEnabled: true,
ExpectedString: "Valid \uf9fd net7.0",
},
{
Case: "valid .fsproj file",
FileName: "Valid.fsproj",
HasFiles: true,
ProjectContents: "...<TargetFramework>net6.0</TargetFramework>...",
ExpectedEnabled: true,
ExpectedString: "Valid \uf9fd net6.0",
},
{
Case: "valid .vbproj file",
FileName: "Valid.vbproj",
HasFiles: true,
ProjectContents: "...<TargetFramework>net5.0</TargetFramework>...",
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)
}
}
}

View file

@ -15,6 +15,7 @@ Supports:
- Poetry project (`pyproject.toml`) - Poetry project (`pyproject.toml`)
- PHP project (`composer.json`) - PHP project (`composer.json`)
- Any nuspec based project (`*.nuspec`, first file match info is displayed) - 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 ## Sample Configuration
@ -42,8 +43,9 @@ Supports:
### Properties ### Properties
| Name | Type | Description | | Name | Type | Description |
| ---------- | -------- | --------------------------- | | ---------- | -------- | ---------------------------------------------------- |
| `.Version` | `string` | The version of your project | | `.Version` | `string` | The version of your project |
| `.Target` | `string` | The target framwork/language version of your project |
| `.Name` | `string` | The name of your project | | `.Name` | `string` | The name of your project |
[templates]: /docs/configuration/templates [templates]: /docs/configuration/templates