Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #22: Implement sorting using a struct to hold the state of the sorting #43

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

aidanmatchette
Copy link

@aidanmatchette aidanmatchette commented Feb 27, 2023

Issue #22 : Using your suggestions I have created a struct to store the state of all of the sorting configurations. This will make it easy to add more configurations in the future with minimal changes to the existing code. As of now "s" will toggle through the sort "Type" i.e. (sortByName / sortByPercentage). If sortByPercentage is currently selected, user can toggle the sortOrder i.e (ascending / descending) by using the "!".

Please give me suggestions for improvements and next steps. 😀

Yet to be implemented

  • Sorting by name
  • Adding the new key-binds to the key-map "register" so users can see the sort options and corresponding key binds
  • Tests - (I am very new to testing in Go, so some direction will be needed here)

Picked two arbitrary keybinds "[" toggles on sortedByPercentage and will
toggle back and forth between Asc and Desc order. "]" turns off sorting
by percentage. Need to add "instructions" to the key map. i.e. "[ toggle
sort by percentage"
@aidanmatchette aidanmatchette changed the title Implement sorting using a struct to hold the state of the sorting Issue #22: Implement sorting using a struct to hold the state of the sorting Feb 27, 2023
Copy link
Owner

@orlangure orlangure left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @aidanmatchette,

Thank you! A few things to consider:

  1. Please use go fmt to format your code, or configure your editor to run it every time you make changes. It makes all Go code look the same, which improves readability and consistency.
  2. Right now this code cannot be merged, because it doesn't support sorting by name. To make sure it works end to end, we have to implement sorting by name. Since you already have the type for it, and a function that sorts based on percentage, it should be fairly simple to add another mode.
  3. sortByCoverage is not needed anymore, you can replace it completely with sort type.

Comment on lines 54 to 55
ASC sortOrder = true
DSC sortOrder = false
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to make these exported if everything else is not exported (use lowercase letters)

// New create a new model that can be used directly in the tea framework.
func New(opts ...Option) *Model {
m := &Model{
activeView: activeViewList,
helpState: helpStateShort,
sortState: SortState{Type: sortStateByName, Order: ASC},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Great way of keeping things backwards compatible 😼

@@ -78,9 +99,11 @@ type Model struct {
detectedPackageName string
requestedFiles map[string]bool
filteredLinesByFile map[string][]int
profilesLoaded []*cover.Profile
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about calling this coverProfiles or loadedProfiles instead?

//toggle on and insiate sortByCoverage (default = Asc)
case "s":
m.toggleSort()
m.Update(m.profilesLoaded)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that internal bubbletea methods should not be called by our code; instead, the framework handles these calls externally, and we only need to react to them. Instead, I suggest to implement a new function updateProfilesList or similar, which checks the current sort state (type + direction), figures out the items in the correct order, and sets them into the list (m.list).

The resulting code should look something like this:

	case "s":
		m.toggleSort()
		return m.updateListItems()

	case "!":
		m.toggleSortOrder()
		return m.updateListItems()

and updateListItems will sort the list based on the state, covert the sorted profiles to list items, and call m.list.setItems.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@orlangure After sorting the list items depending on the SortState, does calling the m.list.SetItems automatically update the UI?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could use some help and direction on sorting by name. Is it best to sort via m.list or m.loadedProfiles. I apologize, I am new to Golang and I am trying to figure out the best way to do this. Any suggestions are greatly appreciated.

func (m *Model) updateListItems()  tea.Cmd {

    switch m.sortState.Type {
    case sortStateByName:
        //sort by name
    case sortStateByPercentage:
        //sort by percentage
        
    }

    return func() tea.Msg {
        return m.loadedProfiles
    }
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After sorting the list items depending on the SortState, does calling the m.list.SetItems automatically update the UI?

Yes, if done properly this will cause the UI to redraw. The flow in bubbletea framework works like "action -> model changes -> rendering", and setting list items is the second step - "model changes". Rendering will follow, and will pick up the updated order of items.

Regarding your second question, below is my draft of the changed functions, based on your code:

func (m *Model) updateListItems() (tea.Model, tea.Cmd) {
	m.sort()

	m.items = make([]list.Item, len(m.profilesLoaded))

	for i, p := range m.profilesLoaded {
		// package name should already be set
		p.FileName = strings.TrimPrefix(p.FileName, m.detectedPackageName+"/")
		m.items[i] = &coverProfile{
			profile:    p,
			percentage: percentCovered(p),
		}
	}

	return m, m.list.SetItems(m.items)
}

func (m *Model) sort() {
	switch m.sortState.Type {
	case sortStateByPercentage:
		m.sortByPercentage()
	case sortStateByName:
		m.sortByName()
	}
}

func (m *Model) sortByPercentage() {
	sort.Slice(m.profilesLoaded, func(i, j int) bool {
		if m.sortState.Order == ASC {
			return percentCovered(m.profilesLoaded[i]) < percentCovered(m.profilesLoaded[j])
		} else {
			return percentCovered(m.profilesLoaded[i]) > percentCovered(m.profilesLoaded[j])
		}
	})
}

func (m *Model) sortByName() {
	sort.Slice(m.profilesLoaded, func(i, j int) bool {
		if m.sortState.Order == ASC {
			return m.profilesLoaded[i].FileName < m.profilesLoaded[j].FileName
		} else {
			return m.profilesLoaded[i].FileName > m.profilesLoaded[j].FileName
		}
	})
}

It is not the most efficient and/or elegant way of solving the problem, but it works. It sorts the "loaded profiles" slice, and then loads the already sorted items into the list.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff! I was playing around with this and nearly had this same implementation, just couldn't figure out some of the Golang syntax. Thanks for this assistance. How would you feel about pulling out this code block to have its own function. This same process is currently used twice in the code. Ref onProfilesLoaded line 206.

	m.items = make([]list.Item, len(m.profilesLoaded))

	for i, p := range m.profilesLoaded {
		// package name should already be set
		p.FileName = strings.TrimPrefix(p.FileName, m.detectedPackageName+"/")
		m.items[i] = &coverProfile{
			profile:    p,
			percentage: percentCovered(p),
		}
	}

I will push my current changes and this PR should update so you can see where I am at.
I have created a model_test.go file, but I am not too sure where to start with that.
And then add the button shortcuts to the UI so users will be able to see how to toggle the sort features.

Thanks as always!

Changes have been made to make sortOrder enum non-exported. We are no
longer directly calling bubbletea Update function, let the framework
handle that by updating m.list. Named changes in some variables and
functions for better clarity.
@orlangure
Copy link
Owner

@aidanmatchette,

I have created a model_test.go file, but I am not too sure where to start with that.

This project uses snapshot tests using "golden" files, see gocovshtest folder for some examples.

This is a CLI utility, and for me the best way to make sure there are as little regressions as possible, is to "remember" how the output looks like, run the program and make sure the output is not changed. After all, the product of this program is some text that is rendered in the terminal.

I use this project for golden file testing: https://github.com/sebdah/goldie, you can play with it a bit.

Then, look at the Taskfile, it has 2 commands that might be useful: one updates the golden files after you add new tests, and the other one updates testdata (mini project that has code coverage report).

@aidanmatchette
Copy link
Author

@orlangure Thanks for that info. I am trying to wrap my head around how the snapshot testing works. I am trying to figure out where to start with this. I could use some direction to get me started on the testing. I have taken a look at goldie and the TaskFile. Should I be writing tests in /internal/model/ , /internal/gocovshtest or both. Thanks for any help you can give!

@orlangure
Copy link
Owner

Look at this file, this is a set of tests of the happy flow. There are examples of navigation and key presses, you should be able to add another case that will toggle the feature multiple times and assert the output. Note that the first time it will obviously fail, so you will need to accept golden files (see Taskfile). Please make sure that the program looks and behaves exactly like you want it to, and only then "save" this expected state to golden files.

@aidanmatchette
Copy link
Author

Sorry I have been a bit slow to respond, busy time at work lately.

I added testing methods to ./intenal/gocovshtest/happy_flow_test.go here for the four sorting states (name / percentage, asc / dsc). I used the Taskfile to update the golden files. Please let me know if I am going in the correct direction.

Thanks as always.

@codecov-commenter
Copy link

codecov-commenter commented Apr 20, 2023

Codecov Report

Patch coverage: 86.44% and project coverage change: +0.30 🎉

Comparison is base (095e557) 86.12% compared to head (fec4a76) 86.42%.

📣 This organization is not using Codecov’s GitHub App Integration. We recommend you install it so Codecov can continue to function properly for your repositories. Learn more

Additional details and impacted files
@@            Coverage Diff             @@
##           master      #43      +/-   ##
==========================================
+ Coverage   86.12%   86.42%   +0.30%     
==========================================
  Files          10       10              
  Lines         807      862      +55     
==========================================
+ Hits          695      745      +50     
- Misses         94       97       +3     
- Partials       18       20       +2     
Impacted Files Coverage Δ
internal/model/options.go 91.17% <0.00%> (-8.83%) ⬇️
internal/model/model.go 92.41% <91.07%> (+0.72%) ⬆️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@orlangure
Copy link
Owner

Hey @aidanmatchette, great progress and definitely the right direction.
A few comments regarding the next steps:

  1. Current test case doesn't cover all cases. We want to make sure that golden files include the following states: by name asc, by name desc, by percentage asc, by percentage desc, and back to the first one. Currently the ! toggle is only applied to one of sort modes.
  2. Sort mode + direction indicators have to be added, because when reviewing the golden files now there is no way of knowing which state they represent. When experimenting with it a bit, I found that due to the lack of visibility, the asc/desc modes are inverted when sorting by percentage. I made a draft of a function that displays mode and direction, without any styling:
    func (m *Model) updateListTitle() {
        var kind, direction string
    
        switch m.sortState.Type {
        case sortStateByName:
    	    kind = "name"
        case sortStateByPercentage:
    	    kind = "percentage"
        }
    
        switch m.sortState.Order {
        case asc:
    	    direction = "asc"
        case dsc:
    	    direction = "dsc"
        }
    
        m.list.Title = fmt.Sprintf("Available files: %s %s", kind, direction)
    }
    You may use it as an example of adding sort indicators. Don't forget that as-is it only adds some text, but we would like to add styling to make the text gray, and maybe to replace asc/desc with up/down arrows.

Let me know if you need any guidance in the above steps. So far you are doing great, it already is usable and helpful. Let's now improve the UX and ship it 😼

@aidanmatchette
Copy link
Author

I'll try to get a PR with some of these changes sometimes this week. Thanks for all your support!

@aidanmatchette
Copy link
Author

I've adjusted the ./intenal/gocovshtest/happy_flow_test.go to account for the four states.
I am working on getting the UI/UX fixed up. Using your function to display the current sort state. How do you intend the styling to look?

I also want to sort options to be added to the keymaps at the bottom, e.x. ^/k up. Is done with an external library, I tried to find where they were being set in the code base, but couldn't find it.

Thanks for all the help!

Screenshot 2023-05-07 at 8 01 43 PM

@orlangure
Copy link
Owner

I'd use the same color as the percentage coverage of every entry in the list (grey color) in the following format right after the word "Available" in the screenshot:

white     grey
Available [sorted by percentage desc]

Using this as the starting point, we can try different options.

Regarding the keymap, I don't see how it is possible out of the box. To extend the default keymap, we should introduce our own list model that wraps the default list model, and only overrides the keymap method used to render the help. Can you try that and let me know if you get stuck on anything?

@aidanmatchette
Copy link
Author

I was reading through the bubbletea/bubble help docs to try to add to the help footer section that displays the keymaps. I added a sort keymap in internal/codeview/keys.go

Screenshot 2023-05-16 at 6 20 08 PM

I then added the new keymap to the DefaultKeyMap and ShortHelp in internal/codeview/code.go. When I built and ran my changes, there was no change in the help footer. This led me to try to look where the other keymaps were being initialized such as the / filter keymap. I was unable to find where that was being set.

How did you previously set the keymaps to display in the help footer?

Thanks for any help or guidance!

@orlangure
Copy link
Owner

The default mappings are set in the "List" component, we don't modify them (currently) and get them for free. That's why I suggested to start using a custom "List" wrapper, that will inherit everything from the regular list, except for the key mappings that we can override.

@aidanmatchette
Copy link
Author

I am trying to figure this problem out. I was reading through the charmbracelet/bubbles docs and saw this. We should be able to make custom key bindings with the default list interface.

I don't know if you have already gone down this road or not, but I also will be looking at implementing our own Model instance to have custom keybindings, but this seems like it might be overkill.

I would love your input.

@orlangure
Copy link
Owner

Sorry for taking long to reply. I looked into custom key bindings injection into "Help" component of "List" model, and couldn't find a quick and easy solution for that. I suggest that we print inline help in the same place we put sort definition. I think what would be "good enough" is something that looks line this:

Available files: %↓ s/! change
Available files: ab↑ s/! change

Percentage sign + ab should make the mode clear, and arrows will show the direction. Just need to come up with a nice style to display this. I'd use default white color bold for mode+direction, dark grey bold for s/! and normal dark grey for "change" word. Inline help part will look exactly like the one at the bottom.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants