There are two APIs we need to finalize: Monaco and VSCode.
Both APIs are identical except in terms of color formatters: VSCode supports a declarative format for color formats, while Monaco supports more generic function-style formatters.
Discussion Points
Color class internally, which has predefined value ranges for color components. RGB is stored in ranges of [0-255], for example. I am more in favor of using floating point [0-1] ranges for all color coordinates in the API. This is what is currently implemented. We just need consensus on the approach.Color.fromHex - I suggest we keep this in proposed state until #32295 is fixed, at which point it can be completely removed.Color Picker should be appeared after clicking on rectangle color symbol (box) like chrome if there is a existed color value otherwise it should be as soon as # sign typed. It is appearing with hovered action now.
@joaomoreno considering the inline box pr is still under review, I'd like to share some thoughts of API finalization of both VSCode and Monaco.
fromHex, fromHSLA, etc in the exposed Color class for now, which can help generate correct rgba value.I would expect VSCode API to be like what's in https://github.com/Microsoft/vscode/pull/32864 .
Exposing this feature to Monaco requires more work, in order to get a consistent API declaration. Currently the API is like
export interface IColorFormatter {
readonly supportsTransparency: boolean;
format(color: Color): string;
}
export interface IColorRange {
range: IRange;
color: IColor;
formatters: IColorFormatter[];
}
export interface IColor {
readonly red: number;
readonly green: number;
readonly blue: number;
readonly alpha: number;
}
It's a bit confusing as the relationship between IColor and Color is not Interface and Implementation. IColor is more likely RGBA. To make it consistent with VSCode API and use float number always, we can change the signature of IColorFormatter to
export interface IColorFormatter {
readonly supportsTransparency: boolean;
format(color: IColor): string;
}
and cast IColor to Color internally, in which way we don't need to expose Color/RGBA/etc and extensions build their own formatters on top of IColor (which is RGBA actually). The catch is we need to do the casing almost everywhere in contrib/colorPicker and tests.
Another way is drop IColor and use RGBA directly
export interface IColorRange {
range: IRange;
color: RGBA;
formatters: IColorFormatter[];
}
This way, we need to expose all Color types Color RGBA HSLA HSVA. They might be more useful to extensions but it makes the API heavier.
fyi - before this all becomes API we need to discuss the color format. it looks like a pretty big stunt and if we want something like that we should at least pick a syntax that is closer to what text mate snippet its variables and transforms look like
It's a bit confusing as the relationship between IColor and Color is not Interface and Implementation.
Yeah, IColorFormatter should work in terms of IColor. That was an oversight.
Another way is drop IColor and use RGBA directly
Definitely interesting, but not yet convinced.
@jrieken based on my understanding of TextMate snippet, we can do slight changes to current format to make it closer to TextMate snippet and share those concepts.
Our current format rgb({red:d[0-255]}, {green:d[0-255]}, {blue:d[0-255]}) is declarative, red, green and blue are Variables and we'll replace these placeholders with corresponding channel. While a typical Variable in TextMate snippet is like \textbf{${TM_SELECTED_TEXT:no text was selected}}.
Their similarities:
{}:Differencies
{} otherwise when we need to set its default value and do formatting, wrap it with {}. Our color format is always surrounded by {}$To make it closer to TM, we can define our color format in following way:
RED, GREEN, BLUE, HUE, SATURATION, LUMINANCE and ALPHA.$, e.g., rgb($RED, $GREEN, $BLUE).[0-255].${«variable»:«new format»«new range»}, e.g. ${RED:f[0-1]}f, float with 2 decimal points.Xf, float with Xd, decimal.x, X, hexadecimal.[LOW:HIGH]Several samples with this new syntax
rgb(${RED}, ${GREEN}, ${BLUE}, ${ALPHA:d[0-255]})#${RED:X}${GREEN:X}${BLUE:X}As we only convert color value from its default format/range to new format/range, a declarative format «new format»«new range» is good enough for now. If ppl have complex feature requests, we can then add Transformations to this. @jrieken look forward to your comments on above proposal.
Great, I think we are on the right path with this. Some more thoughts...
${var_name:default value} is just one aspect of it and I don't know if it's the best fit because the semantics are: "Insert the resolved value of var_name and if that isn't possible insert default value. In our case we do have values for $RED, $GREEN etc. and I think we need something else.In order to stay closer to the semantics and features of TextMate I can think of the following
$RED_HEX, $RED_FLOAT, $RED_DEC etc. Next is number ranges and I believe the most common color ranges are [0-1], [0-100], [0-255], and [0-360]. I might be mistaken but could add more variables, e.g $HUE_DEC_360.$RED_HEX[0-255] which then will be interpreted as todays {blue:X[0-255]}. The challenge is that it's not well defined what a variable name is. From my experience it is [a-zA-Z_][_a-zA-Z0-9]* so it needs some tweaking or a different format. ${«variable»/«regexp»/«format»/«options»} A real world sample that inserts your user name with the first latter capitalised is this: Hello ${USER/./${0:/upcase}/}. So, in original text mate the format-string supports built-in modifiers, like /upcase etc. I am looking at those modifiers and we could have something in the same spirit, e.g ${RED/.*/${0:/toDecimal(0,255)}/} which admittedly looks very complex...To conclude, my preferences are option 1 and 2 or something in-between them. Our snippet parser and controller support all of this already today and it just needs another snippet variable resolver. I personally need to get a better understanding of the color formats because I don't understand red:d[0-255] when I can only create a Color object with numbers between 0 and 1. That makes me believe the 0 isn't actually needed in the formats. Please enlighten me @joaomoreno.
0 just serves as a minimum, as you could also have red:d[-128:128] although that is a very far fetched example... so I think we could drop the minimum and just go with red:d[255] or similar.
Ok, so my proposal would be dynamic variables ala $RED_HEX_FF. That would allows us to dump all custom parsing logic, make the API use the SnippetString and make the implementation use the SnippetController. So, I believe the format for color variable names would be
color -> '$_' name ('_' format)?
name -> 'red' | 'green' | 'blue' | 'hue' | 'saturation' | 'luminance' | 'alpha'
format -> ('d' | 'x' | 'f' | 'f' decimal) ('_' decimal){0-2}
Which allows for $red_x_ff (upper bound) or $red_x_00_ff (lower/upper bound) etc. This isn't the prettiest and bends a little the meaning of variables names but if we want to ship this month we have little slack for other experiments.
Now, seeing these names I might prefer something more transform-like, e.g ${RED/toDecimal(0,255)} which isn't adhering to the text mate snippet syntax but maybe something we can invent...
Alternatively to the discussions above I'd propose we stop the effort in trying to describe all possible color representations on this planet and simply delegate this to the extension in question. So, instead of a template string, we simply ask the provider for the insert string of a color. It will adding to the provider something like this
interface ColorProvider {
resolveColor(color: Color): ProviderResult<string>
}
So, while the color changes (or on commit) we ask the provider to turn it into a string. Yeah, more chatty but more conservative, less API exposure etc.
That was the original approach, of course. I'll write down the missing drawbacks:
With formats, we're obviously optimising towards responsiveness. I'm still convinced it is the way to go, especially considering that the argument against it is purely not being sure about the format syntax.
I personally don't have an opinion on the syntax itself. We can align it better with TextMate, but I doubt we'll change the experience for anyone for the better. Developers will still need to read the documentation to learn how to use them, no matter how many snippets they've written in the past.
There won't be any label when the extension host is busy, which it can bee since while we pick a new color, we update the model, which can trigger extensions to do some work
Well, with todays implementation we keep it very busy by sending hundreds of document change events while dragging around in the picker UI. Also something we don't want and something we need to throttle.
The general truth is that the editor doesn't work well when the extension host is busy. However, that also applies to format on type, code lens, resolving completions, typing etc. So, I wonder if you have actually implemented and tested that or is this merely a feeling?
It's really just a feeling... it would actually be cool to see it live.
resolveColor
I don't have a strong opinion on this. When we ask provideDocumentColors, extensions need to convert to color from whatever to RGBA. Considering they have the toolset to do the transformation, resolveColor can convert RGBA to the target format with the same toolset, e.g. parse-color.
To support multiple formats, resolveColor should better return string[] which does toString for all formats.
The label in the UI can possibly be laggy due to the roundtrip
That lag will be aggravated when we start throttling the calls, which we must
This pushes me more to declarative format as we need throttling and we don't want to make it laggy.
with todays implementation we keep it very busy by sending hundreds of document change events while dragging around in the picker UI
@alexandrudima pointed this out to me in this morning as well and I did some basic testing. I didn't check whether it makes extensions laggy but dragging around for a few seconds, the CPU usage increases 50% and even 100% sometimes (well if you keep dragging it for 5 seconds, you can see a 100+% Code Helper).
No one complains about it yet but I'd like to have it fix to avoid another HN post.
I'll add more info ...
Current format
const CSSColorFormats = {
Hex: '#{red:X}{green:X}{blue:X}',
RGB: {
opaque: 'rgb({red:d[0-255]}, {green:d[0-255]}, {blue:d[0-255]})',
transparent: 'rgba({red:d[0-255]}, {green:d[0-255]}, {blue:d[0-255]}, {alpha})'
},
HSL: {
opaque: 'hsl({hue:d[0-360]}, {saturation:d[0-100]}%, {luminance:d[0-100]}%)',
transparent: 'hsla({hue:d[0-360]}, {saturation:d[0-100]}%, {luminance:d[0-100]}%, {alpha})'
}
};
Dynamic TextMate Snippet Variable
const CSSColorFormats = {
Hex: '#${red_x}${green_x}${blue_x}',
RGB: {
opaque: 'rgb(${red_d_0_255}, ${green_d_0_255}, ${blue_d_0_255})',
transparent: 'rgba(${red_d_0_255}, ${green_d_0_255}, ${blue_d_0_255}, ${alpha})'
},
HSL: {
opaque: 'hsl(${hue_d_0_360}, ${saturation_d_0_100}%, ${luminance_0_100}%)',
transparent: 'hsla(${hue_d_0_360}, ${saturation_d_0_100}%, ${luminance_d_0_100}%, ${alpha})'
}
};
Trying to organise and structure my thoughts here... I have two main concerns, one is regarding the declarative nature and one is the constantly changing document
Declarative Color Format
It definitely looks appealing and is cleverly implemented however it's not free of worry to me. These are my concerns
toDecimalColor.FromArgb(int) with special byte order for that int. We might be able to express that but it might get ugly with un-padded values (F vs 0F). Then there are also well-known constants for colors, e.g. #FFF0F8FF is Color.AliceBlue and I don't see how that is expressed in the color format string.Document Change Event Avalanche
This isn't really an API concern but an implementation concern. Today, while dragging around the model is being updated, these changes are forwarded to the extension host and language servers. I am unsure how we cope with that. I believe this is comparable to previewing IntelliSense suggestions on focus vs. inserting them on select and I'd feel better if we make this a UI thing and not a model change.
In short, my suggestion would be to execute code that transforms a color into one or many strings or text edits. Unsure how often we call that, personally I wouldn't feel uncomfortable if the document only changes on commit. Then I don't feel like I am loosing my current state. Updating the label inside the widget is another story, but we could also show two labels. A fixed color format (RGBA) and the 'preview' change that the provider will do.
@rebornix fyi - I have one or two little nits we should discuss around the new API but we can put that in a different issue
Was verifying this, but the API still lives in the proposed dts? I thought the plan is to move it to stable during Sep.
no
Most helpful comment
I don't have a strong opinion on this. When we ask
provideDocumentColors, extensions need to convert to color from whatever to RGBA. Considering they have the toolset to do the transformation,resolveColorcan convert RGBA to the target format with the same toolset, e.g.parse-color.To support multiple formats,
resolveColorshould better returnstring[]which doestoStringfor all formats.This pushes me more to declarative format as we need throttling and we don't want to make it laggy.
@alexandrudima pointed this out to me in this morning as well and I did some basic testing. I didn't check whether it makes extensions laggy but dragging around for a few seconds, the CPU usage increases 50% and even 100% sometimes (well if you keep dragging it for 5 seconds, you can see a 100+% Code Helper).
No one complains about it yet but I'd like to have it fix to avoid another HN post.
I'll add more info ...