From b47e057f1440926756933299b78fba96c69b0511 Mon Sep 17 00:00:00 2001 From: Jacek W Date: Mon, 14 Feb 2022 08:41:33 +0100 Subject: [PATCH] feat: UI5 tooling segment --- docs/docs/segment-ui5tooling.md | 61 ++++++++++++ docs/sidebars.js | 1 + src/engine/segment.go | 3 + src/environment/shell.go | 20 ++++ src/mock/environment.go | 5 + src/segments/ui5tooling.go | 49 ++++++++++ src/segments/ui5tooling_test.go | 168 ++++++++++++++++++++++++++++++++ themes/schema.json | 29 +++++- 8 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 docs/docs/segment-ui5tooling.md create mode 100644 src/segments/ui5tooling.go create mode 100644 src/segments/ui5tooling_test.go diff --git a/docs/docs/segment-ui5tooling.md b/docs/docs/segment-ui5tooling.md new file mode 100644 index 00000000..2cd83622 --- /dev/null +++ b/docs/docs/segment-ui5tooling.md @@ -0,0 +1,61 @@ +--- +id: ui5tooling +title: UI5 Tooling +sidebar_label: UI5 Tooling +--- + +## What + +Display the active [UI5 tooling][ui5-homepage] version (global or local if present - +see [the documentation][ui5-version-help]). + +## Sample Configuration + +```json +{ + "background": "#f5a834", + "foreground": "#100e23", + "powerline_symbol": "\ue0b0", + "properties": { + "template": " \ufab6ui5 {{ .Full }} " + }, + "style": "powerline", + "type": "ui5tooling" +} +``` + +## Properties + +- home_enabled: `boolean` - display the segment in the HOME folder or not - defaults to `false` +- fetch_version: `boolean` - display the UI5 tooling version - defaults to `true` +- display_error: `boolean` - show the error context when failing to retrieve the version information - defaults to `true` +- missing_command_text: `string` - text to display when the java command is missing - defaults to empty +- display_mode: `string` - determines when the segment is displayed + - `always`: the segment is always displayed + - `files`: the segment is only displayed when `*ui5*.y(a)ml` file is present in the current folder + - `context`: (default) the segment is only displayed when `*ui5*.y(a)ml` file is present in the current folder + or it has been found in the parent folders (check up to 4 levels) + +## Template ([info][templates]) + +:::note default template + +```template +{{ if .Error }}{{ .Error }}{{ else }}{{ .Full }}{{ end }} +``` + +::: + +## Template Properties + +- `.Full`: `string` - the full version +- `.Major`: `string` - major number +- `.Minor`: `string` - minor number +- `.Patch`: `string` - patch number +- `.Prerelease`: `string` - prerelease info text +- `.BuildMetadata`: `string` - build metadata +- `.Error`: `string` - when fetching the version string errors + +[templates]: /docs/config-templates +[ui5-homepage]: https://sap.github.io/ui5-tooling +[ui5-version-help]: https://sap.github.io/ui5-tooling/pages/CLI/#ui5-versions diff --git a/docs/sidebars.js b/docs/sidebars.js index b3550d80..5b248084 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -76,6 +76,7 @@ module.exports = { "terraform", "text", "time", + "ui5tooling", "wakatime", "wifi", "winreg", diff --git a/src/engine/segment.go b/src/engine/segment.go index 4acdab52..f7ab8f11 100644 --- a/src/engine/segment.go +++ b/src/engine/segment.go @@ -149,6 +149,8 @@ const ( IPIFY SegmentType = "ipify" // HASKELL segment HASKELL SegmentType = "haskell" + // UI5 Tooling segment + UI5TOOLING SegmentType = "ui5tooling" ) func (segment *Segment) shouldIncludeFolder() bool { @@ -266,6 +268,7 @@ func (segment *Segment) mapSegmentWithWriter(env environment.Environment) error BREWFATHER: &segments.Brewfather{}, IPIFY: &segments.IPify{}, HASKELL: &segments.Haskell{}, + UI5TOOLING: &segments.UI5Tooling{}, } if segment.Properties == nil { segment.Properties = make(properties.Map) diff --git a/src/environment/shell.go b/src/environment/shell.go index e0b8d54f..8c3eea59 100644 --- a/src/environment/shell.go +++ b/src/environment/shell.go @@ -150,6 +150,7 @@ type Environment interface { HasFilesInDir(dir, pattern string) bool HasFolder(folder string) bool HasParentFilePath(path string) (fileInfo *FileInfo, err error) + HasFileInParentDirs(pattern string, depth uint) bool HasCommand(command string) bool FileContent(file string) string FolderList(path string) []string @@ -319,6 +320,25 @@ func (env *ShellEnvironment) HasFilesInDir(dir, pattern string) bool { return len(matches) > 0 } +func (env *ShellEnvironment) HasFileInParentDirs(pattern string, depth uint) bool { + defer env.trace(time.Now(), "HasFileInParent", pattern, fmt.Sprint(depth)) + currentFolder := env.Pwd() + + for c := 0; c < int(depth); c++ { + if env.HasFilesInDir(currentFolder, pattern) { + return true + } + + if dir := filepath.Dir(currentFolder); dir != currentFolder { + currentFolder = dir + } else { + return false + } + } + + return false +} + func (env *ShellEnvironment) HasFolder(folder string) bool { defer env.trace(time.Now(), "HasFolder", folder) _, err := os.Stat(folder) diff --git a/src/mock/environment.go b/src/mock/environment.go index 147dd608..c7547180 100644 --- a/src/mock/environment.go +++ b/src/mock/environment.go @@ -209,3 +209,8 @@ func (env *MockedEnvironment) MockGitCommand(dir, returnValue string, args ...st args = append([]string{"-C", dir, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...) env.On("RunCommand", "git", args).Return(returnValue, nil) } + +func (env *MockedEnvironment) HasFileInParentDirs(pattern string, depth uint) bool { + args := env.Called(pattern, depth) + return args.Bool(0) +} diff --git a/src/segments/ui5tooling.go b/src/segments/ui5tooling.go new file mode 100644 index 00000000..ccd920ac --- /dev/null +++ b/src/segments/ui5tooling.go @@ -0,0 +1,49 @@ +package segments + +import ( + "oh-my-posh/environment" + "oh-my-posh/properties" +) + +const UI5ToolingYamlPattern = "*ui5*.y*ml" + +type UI5Tooling struct { + language + HasUI5YamlInParentDir bool +} + +func (u *UI5Tooling) Template() string { + return languageTemplate +} + +func (u *UI5Tooling) Init(props properties.Properties, env environment.Environment) { + u.language = language{ + env: env, + props: props, + extensions: []string{UI5ToolingYamlPattern}, + loadContext: u.loadContext, + inContext: u.inContext, + displayMode: props.GetString(DisplayMode, DisplayModeContext), + commands: []*cmd{ + { + executable: "ui5", + args: []string{"--version"}, + regex: `(?:(?P((?P[0-9]+).(?P[0-9]+).(?P[0-9]+))))`, + }, + }, + versionURLTemplate: "https://github.com/SAP/ui5-cli/releases/tag/v{{ .Full }}", + } +} + +func (u *UI5Tooling) Enabled() bool { + return u.language.Enabled() +} + +func (u *UI5Tooling) loadContext() { + // for searching ui5 yaml from subdirectories of UI5 project root - up to 4 levels + u.HasUI5YamlInParentDir = u.env.HasFileInParentDirs(UI5ToolingYamlPattern, 4) +} + +func (u *UI5Tooling) inContext() bool { + return u.HasUI5YamlInParentDir +} diff --git a/src/segments/ui5tooling_test.go b/src/segments/ui5tooling_test.go new file mode 100644 index 00000000..6af56631 --- /dev/null +++ b/src/segments/ui5tooling_test.go @@ -0,0 +1,168 @@ +package segments + +import ( + "fmt" + "oh-my-posh/environment" + "oh-my-posh/mock" + "oh-my-posh/properties" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + WorkingDirRoot = "/home/user/dev/my-app" +) + +type testCase struct { + Case string + Template string + ExpectedString string + ExpectedEnabled bool + UI5YamlFilename string + WorkingDir string + Version string + DisplayMode string +} + +func TestUI5Tooling(t *testing.T) { + cases := []testCase{ + { + Case: "1) ui5tooling 2.12.1 - file ui5.yaml present in cwd; DisplayMode = files", + ExpectedString: "2.12.1", + ExpectedEnabled: true, + UI5YamlFilename: "ui5.yaml", + Version: `2.12.1 (from C:\somewhere\cli\bin\ui5.js)`, + DisplayMode: DisplayModeFiles, + }, + { + Case: "2) ui5tooling 2.12.2 - file ui5.yaml present in cwd; default display mode (context)", + ExpectedString: "2.12.2", + ExpectedEnabled: true, + UI5YamlFilename: "ui5.yaml", + Version: `2.12.2 (from C:\somewhere\cli\bin\ui5.js)`, + }, + { + Case: "3) ui5tooling 2.12.3 - file ui5.yaml present; cwd is sub dir, default display mode (context)", + ExpectedString: "2.12.3", + WorkingDir: WorkingDirRoot + "/subdir", + ExpectedEnabled: true, + UI5YamlFilename: "ui5.yaml", + Version: `2.12.3 (from C:\somewhere\cli\bin\ui5.js)`, + }, + { + Case: "4) no ui5tooling segment - file ui5.yaml present, cwd is sub dir; display mode = files", + ExpectedString: "", + WorkingDir: WorkingDirRoot + "/subdir", + ExpectedEnabled: false, + UI5YamlFilename: "ui5.yaml", + DisplayMode: DisplayModeFiles, + Version: `2.12.1 (from C:\somewhere\cli\bin\ui5.js)`, + }, + { + Case: "5) ui5tooling 2.12.4 - file ui5-dist.yml present in cwd", + ExpectedString: "2.12.4", + ExpectedEnabled: true, + UI5YamlFilename: "ui5-dist.yml", + Version: `2.12.4 (from C:\somewhere\cli\bin\ui5.js)`, + DisplayMode: DisplayModeFiles, + }, + { + Case: "6) no ui5tooling segment - file ui5.yaml not present, display mode = files", + ExpectedString: "", + ExpectedEnabled: false, + Version: `2.12.1 (from C:\somewhere\cli\bin\ui5.js)`, + DisplayMode: DisplayModeFiles, + }, + { + Case: "7) no ui5tooling segment - file ui5.yaml not present, default display mode (context)", + ExpectedString: "", + ExpectedEnabled: false, + Version: `2.12.1 (from C:\somewhere\cli\bin\ui5.js)`, + }, + { + Case: "8) ui5tooling 11.0.0-rc1, no ui5.yaml file but display mode = always", + Template: "{{ .Major }}", + ExpectedString: "11", + ExpectedEnabled: true, + Version: `11.0.0-rc1 (from C:\somewhere\cli\bin\ui5.js)`, + DisplayMode: DisplayModeAlways, + }, + } + + for _, tc := range cases { + env := prepareMockedEnvironment(&tc) + ui5tooling := &UI5Tooling{} + + if tc.WorkingDir == "" { + tc.WorkingDir = WorkingDirRoot + } + + if tc.DisplayMode == "" { + tc.DisplayMode = DisplayModeContext + } + + if tc.Template == "" { + tc.Template = ui5tooling.Template() + } + + props := properties.Map{ + DisplayMode: tc.DisplayMode, + } + + ui5tooling.Init(props, env) + err := mockFilePresence(&tc, ui5tooling, env) + + if err != nil { + t.Fail() + } + + failMsg := fmt.Sprintf("Failed in case: %s", tc.Case) + assert.Equal(t, tc.ExpectedEnabled, ui5tooling.Enabled(), failMsg) + assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, ui5tooling), failMsg) + } +} + +func prepareMockedEnvironment(tc *testCase) *mock.MockedEnvironment { + var env = new(mock.MockedEnvironment) + env.On("HasCommand", "ui5").Return(true) + env.On("RunCommand", "ui5", []string{"--version"}).Return(tc.Version, nil) + env.On("Home").Return("/home/user") + env.On("Pwd").Return(WorkingDirRoot) + + env.On("TemplateCache").Return(&environment.TemplateCache{ + Env: make(map[string]string), + }) + + return env +} + +func mockFilePresence(tc *testCase, ui5tooling *UI5Tooling, env *mock.MockedEnvironment) error { + for _, f := range ui5tooling.language.extensions { + match, err := filepath.Match(f, tc.UI5YamlFilename) + + if err != nil { + return err + } + + if match { + if tc.DisplayMode == DisplayModeFiles && tc.WorkingDir == WorkingDirRoot { + env.On("HasFiles", f).Return(true) + env.On("HasFileInParentDirs", f, uint(4)).Return(false) + // mode context, working dir != working dir root + } else if tc.DisplayMode == DisplayModeContext { + env.On("HasFileInParentDirs", f, uint(4)).Return(false) + env.On("HasFiles", f).Return(true) + } else { + env.On("HasFileInParentDirs", f, uint(4)).Return(false) + env.On("HasFiles", f).Return(false) + } + } else { + env.On("HasFileInParentDirs", f, uint(4)).Return(false) + env.On("HasFiles", f).Return(false) + } + } + + return nil +} diff --git a/themes/schema.json b/themes/schema.json index f6d37ff4..b89c436a 100644 --- a/themes/schema.json +++ b/themes/schema.json @@ -183,7 +183,8 @@ "winreg", "plastic", "ipify", - "haskell" + "haskell", + "ui5tooling" ] }, "style": { @@ -1907,6 +1908,32 @@ } } } + }, + { + "if": { + "properties": { + "type": { "const": "ui5tooling" } + } + }, + "then": { + "title": "UI5 tooling CLI segment", + "description": "https://ohmyposh.dev/docs/ui5tooling", + "properties": { + "properties": { + "properties": { + "fetch_version": { + "$ref": "#/definitions/fetch_version" + }, + "display_mode": { + "$ref": "#/definitions/display_mode" + }, + "missing_command_text": { + "$ref": "#/definitions/missing_command_text" + } + } + } + } + } } ] }