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
Add styled
and styled-segment
builtins
#674
Conversation
With the last commit |
Hi! Thank you so much for your contribution. Really impressed by the progress you brought. I will take a look later this week - a bit occupied lately. |
Hi - I haven't been able to look at this last week. I will definitely (TM) take a look this week! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks again for your contribution! Please see review comments.
You said that you wouldn't be able to work on this for a while. Please tell me if that is still the case; if so, it makes most sense for me to merge this into a feature branch, and either you or I can finish the work here.
edit/styled.go
Outdated
@@ -1,30 +1,28 @@ | |||
package edit | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove edit:styled
in favor of builtin:styled
you introduced.
I realized that builtin:styled
's API is not completely backward-compatible with the old edit:styled
. This is OK; Elvish is not maintaining API compatibility at the moment.
eval/builtin_fn_styled.go
Outdated
// from the supplied options applied to it. If the input is already a Segment its | ||
// attributes are copied and modified. | ||
func styledSegment(options RawOptions, input interface{}) (*styled.Segment, error) { | ||
switch input := input.(type) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a lot of common code in the two branches. In fact, they only differ near the end.
The current way does have the advantage of doing the type check early though. Probably you can do an explicit type check first, like:
switch input.(type) {
case string, styled.Segment:
default:
return nil, errStyledSegmentArgType
}
...
eval/builtin_fn_styled.go
Outdated
}, nil | ||
} | ||
|
||
return nil, errors.New("expected string or styled segment") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Declare an error in the global scope and use it:
var errStyledSegmentArgType = errors.New("argument to styled-segment must be string or styled segment")
eval/builtin_fn_styled.go
Outdated
switch transformedSegment := vs[0].(type) { | ||
case styled.Segment: | ||
text[i] = transformedSegment | ||
case *styled.Segment: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this branch possible at all?
Having to handle both styled.Segment
and *styled.Segment
is a code smell; it means that the code is not using one type exclusively.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This applied to styledSegment
as well and was needed because sometimes they would get a pointer and sometimes not. The error was that I was passing the non-pointer segment to the user supplied function. It is gone in the refactored code.
eval/builtin_fn_styled_test.go
Outdated
"bg-white": "107", | ||
} | ||
|
||
func TestCompatibility(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test tests builtin transformers extensively, but doesn't test for other behaviors:
- Construction of styled segments (either by supplying a string and styles, or an existing styled segment and styles);
- Field access of styled segments;
- Applying multiple transformers with
styled
; - Using literal function as transformers with
styled
.
I don't think we necessarily need to test every builtin transformer: testing one from each category (text style, foreground, background) should be sufficient. This way you can write out the tests manually; which is also desirable because you now avoid having logics in tests.
Note that I changed the test framework API slightly - I renamed runTests
to Test
and made it take test cases as variadic arguments. Your test will now look like:
Test(t,
That("print (styled abc bold)").Prints(...),
That("print (styled abc red)").Prints(...)
)
styled/colors.go
Outdated
|
||
import "fmt" | ||
|
||
type Color uint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type safety and memory compactness aside, it seems to be much more straightforward if we just use string
to represent colors and translate them to SGR codes when printing.
It will also be easier to extend more complex color representations (e.g. hex #123456
) if we just use string
.
styled/styled.go
Outdated
) | ||
|
||
type TextStyle struct { | ||
bold *bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is nil
(lack of an attribute) and false
(explicit false) a useful distinction? I think not - just make the fields bool
and the code can be simplified quite a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're right. Honestly I don't remember my reasoning from then. Probably for easier merging...
But I think we need transformers for styled
to undo bolding, diming, ... Would you accept to call them un-...
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. no-
is probably a better prefix.
eval/builtin_fn_styled.go
Outdated
|
||
transformers := make(map[interface{}]interface{}) | ||
for k, v := range styled.SegmentTransformers { | ||
transformers[k] = NewBuiltinFn("style-transform "+k, v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These builtins are not actually accessible from the builtin:
namespace. Give them more funny names, like <style transformer xxx>
.
Speaking of which, I thought a bit more about the API, and I think that exposing transformers as a writable map is likely a bad idea: this is essentially a global variable, and it is hard to coordinate access from different modules written by different people. For instance, one module may install a transformer named foo
and use it later, while another may install a transformer with the same name foo
. One of the two modules will use a transformer that it does not intend to use.
My amended proposal is to not expose this transformer map at all and keep it strictly internal. This part of the code can be removed.
In fact, after removing the transformer map, the builtin transformers do not even need to live in a map; the styled
command can also parse the transformer programmatically.
Thank you for your comments. I will have a more in depth look at them this evening. Thinking about this: Do we really need to distinguish these two parts? |
Re. distinguishing terminal and HTML/CSS outputs: you are right that for a web interface to work we will need to convert ANSI sequences to HTML/CSS anyway, so it makes sense to just implementation styled -> ANSI conversion and let ANSI -> HTML/CSS do the rest. But I see some disadvantages to this approach.
|
OK, I rebased to the latest master and adressed all of your comments.
Probably there could be even more tests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! And sorry for the late response again.
I have some relatively minor comments, which hopefully is easy to address.
Another general comment: I feel that using unmarked names to mean "toggle" is a little too subtle; I would rather bold
to always set text to bold, and have a toggle-bold
that toggles boldness. But I am not requiring you to address that in this PR.
styled/styled.go
Outdated
} | ||
|
||
func (s *Style) ImportFromOptions(options map[string]interface{}) error { | ||
assignColor := func(key string, assign func(string)) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second argument can be a *string
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I knew there has to be a non awkward way to do this. Thx!
styled/styled.go
Outdated
return nil | ||
} | ||
|
||
func IsValidColorString(col string) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not need to be exported. It is also probably better called isValidColorName
.
I am a little torn about how you are encoding a string set using a switch statement. It is indeed the most concise way though; let's keep it as is for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're absolutely right on both points...
styled/styled.go
Outdated
} | ||
return nil | ||
} | ||
assignBool := func(key string, assign func(bool)) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idem.
styled/segment.go
Outdated
"github.com/elves/elvish/eval/vals" | ||
) | ||
|
||
// A styled Segment is a string that has some style applied to it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docstrings for exported symbols should start with the symbol itself (https://golang.org/doc/effective_go.html#commentary). In this case, simply remove "A styled", which is apparent from the package it is in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright I will rework and add missing docstrings.
styled/segment.go
Outdated
|
||
func (Segment) Kind() string { return "styled-segment" } | ||
|
||
// Returns the representation of this Segment. The string can be used to construct |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Start the docstring with Repr
, so it reads "Repr returns ...". Same for docstrings below.
import ( | ||
"fmt" | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add docstring.
Generally, exported symbols should have docstrings, the only exception being docstrings are optional for methods implementing interface.
Blink bool | ||
Inverse bool | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be a variable. "DefaultStyle
" is a better name; "empty" is normally understood to be StructName{}
in Go.
Speaking of which, it probably makes sense to use the empty string for default colors; this makes Style{}
a valid value, which is considered good practice in Go. But I am not requiring you to do this in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also thought about defining empty strings to be the default color but wasn't sure how to handle the style transformer names. They should still be called default
and bg-default
but I had the thought that it could be inconsistent...
Should the representation of segments contain the default color?
Again I will work on this this evening after work. While doing this I could also add more (and better named) transformers. I will make them uniform so that for each attribute there will be setting, unsetting and toggling. |
Done. |
Now there are three transformers for each boolean style attribute that allow setting, unsetting and toggling the corresponding attribute.
Marvelous. Thank you! |
@xiaq @fehnomenal this is an awesome change! I look forward to exploring all the new capabilities. If I may suggest, removing |
Initial transformation to work with the new styled function (elves/elvish#674)
These commits add two new builtins
styled
andstyled-segment
that are modelled after @xiaq's design in #520. It allows nesting, querying for style attributes and defining custom style transformers.Currently the new functions are not used anywhere. The problem is that
ui.Styled
andui.Styles
are used to render the editor (completion matching, history, ...). This is done for the individual parts of the UI.Probably there should be something like a renderer interface that is implemented by
shell.Shell
andweb.Web
and allows rendering of more abstract things like a selection list, notifications and so on.This is probably a rather large refactoring that needs to be coordinated and unfortunately I'm very short on time at the moment. So I decided to publish at least my current progress.