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

Completion kind Color for color keywords and hex 4 / hex 8 #346

Merged
merged 6 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/languageFacts/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import * as nodes from '../parser/cssNodes';

import * as l10n from '@vscode/l10n';

const hexColorRegExp = /(^#([0-9A-F]{3}){1,2}$)|(^#([0-9A-F]{4}){1,2}$)/i;

export const colorFunctions = [
{
label: 'rgb',
Expand Down Expand Up @@ -132,6 +134,8 @@ export const colorFunctions = [
},
];

const colorFunctionNameRegExp = /^(rgb|rgba|hsl|hsla|hwb)$/i;

export const colors: { [name: string]: string } = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
Expand Down Expand Up @@ -283,11 +287,15 @@ export const colors: { [name: string]: string } = {
yellowgreen: '#9acd32'
};

const colorsRegExp = new RegExp(`^(${Object.keys(colors).join('|')})$`, "i");
Copy link
Contributor

Choose a reason for hiding this comment

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

That's a huge regex. We can just do a lookup in the colors object instead

Copy link
Contributor Author

@romainmenke romainmenke May 26, 2023

Choose a reason for hiding this comment

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

This should be checked ascii case insensitive.
https://github.com/web-platform-tests/wpt/blob/master/css/css-color/parsing/color-invalid-named-color.html#L32

// Not a real upper case `K`
'blacK' === 'black'
// false
'blacK'.toLowerCase() === 'black'
// true -> incorrect
/black/i.test('blacK')
// false

vs.

// Real upper case `K`
'blacK' === 'black'
// false
'blacK'.toLowerCase() === 'black'
// true
/black/i.test('blacK')
// true

But maybe it's fine to be a little bit less strict here?

Copy link
Contributor

@aeschli aeschli May 26, 2023

Choose a reason for hiding this comment

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

Interesting, I didn't know that toLowerCase does that. We use it over the place to do case insensitive comparisons.
I suggest to ignore that for now.


export const colorKeywords: { [name: string]: string } = {
'currentColor': 'The value of the \'color\' property. The computed value of the \'currentColor\' keyword is the computed value of the \'color\' property. If the \'currentColor\' keyword is set on the \'color\' property itself, it is treated as \'color:inherit\' at parse time.',
'transparent': 'Fully transparent. This keyword can be considered a shorthand for rgba(0,0,0,0) which is its computed value.',
};

const colorKeywordsRegExp = new RegExp(`^(${Object.keys(colorKeywords).join('|')})$`, "i");

function getNumericValue(node: nodes.Node, factor: number) {
const val = node.getText();
const m = val.match(/^([-+]?[0-9]*\.?[0-9]+)(%?)$/);
Expand Down Expand Up @@ -331,9 +339,14 @@ export function isColorConstructor(node: nodes.Function): boolean {
if (!name) {
return false;
}
return /^(rgb|rgba|hsl|hsla|hwb)$/gi.test(name);
return colorFunctionNameRegExp.test(name);
}

export function isColorString(s: string) {
return hexColorRegExp.test(s) || colorsRegExp.test(s) || colorKeywordsRegExp.test(s);
}


/**
* Returns true if the node is a color value - either
* defined a hex number, as rgb or rgba function, or
Expand Down Expand Up @@ -481,7 +494,7 @@ export function hslFromColor(rgba: Color): HSLA {
export function colorFromHWB(hue: number, white: number, black: number, alpha: number = 1.0): Color {
if (white + black >= 1) {
const gray = white / (white + black);
return {red: gray, green: gray, blue: gray, alpha};
return { red: gray, green: gray, blue: gray, alpha };
}

const rgb = colorFromHSL(hue, 1, 0.5, alpha);
Expand Down Expand Up @@ -537,11 +550,11 @@ export function getColorValue(node: nodes.Node): Color | null {
if (lastValue instanceof nodes.BinaryExpression) {
const left = lastValue.getLeft(), right = lastValue.getRight(), operator = lastValue.getOperator();
if (left && right && operator && operator.matches('/')) {
colorValues = [ colorValues[0], colorValues[1], left, right ];
colorValues = [colorValues[0], colorValues[1], left, right];
}
}
}
}
}
}
if (!name || colorValues.length < 3 || colorValues.length > 4) {
return null;
Expand Down
9 changes: 2 additions & 7 deletions src/services/cssCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ export class CSSCompletion {
sortText: SortTexts.Variable
};

if (typeof completionItem.documentation === 'string' && isColorString(completionItem.documentation)) {
if (typeof completionItem.documentation === 'string' && languageFacts.isColorString(completionItem.documentation)) {
completionItem.kind = CompletionItemKind.Color;
}

Expand Down Expand Up @@ -466,7 +466,7 @@ export class CSSCompletion {
textEdit: TextEdit.replace(this.getCompletionRange(null), symbol.name),
kind: CompletionItemKind.Variable
};
if (typeof completionItem.documentation === 'string' && isColorString(completionItem.documentation)) {
if (typeof completionItem.documentation === 'string' && languageFacts.isColorString(completionItem.documentation)) {
completionItem.kind = CompletionItemKind.Color;
}

Expand Down Expand Up @@ -1147,8 +1147,3 @@ function getCurrentWord(document: TextDocument, offset: number): string {
}
return text.substring(i + 1, offset);
}

function isColorString(s: string) {
// From https://stackoverflow.com/questions/8027423/how-to-check-if-a-string-is-a-valid-hex-color-representation/8027444
return (s.toLowerCase() in languageFacts.colors) || /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(s);
}
30 changes: 30 additions & 0 deletions src/test/css/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,36 @@ suite('CSS - Completion', () => {
{ label: '--borderwidth', documentation: undefined, resultText: 'body { border-left: --borderwidth; border-right: var(--borderwidth ' },
]
});
await testCompletionFor('a { color: | } :root { --color-hex3: #f00; --color-hex4: #F007; --color-hex6: #ff0000; --color-hex8: #ff000077; --color-named: black; --color-keyword: currentColor; }', {
items: [
{ label: '--color-hex3', kind: CompletionItemKind.Color, resultText: 'a { color: var(--color-hex3) } :root { --color-hex3: #f00; --color-hex4: #F007; --color-hex6: #ff0000; --color-hex8: #ff000077; --color-named: black; --color-keyword: currentColor; }' },
{ label: '--color-hex4', kind: CompletionItemKind.Color, resultText: 'a { color: var(--color-hex4) } :root { --color-hex3: #f00; --color-hex4: #F007; --color-hex6: #ff0000; --color-hex8: #ff000077; --color-named: black; --color-keyword: currentColor; }' },
{ label: '--color-hex6', kind: CompletionItemKind.Color, resultText: 'a { color: var(--color-hex6) } :root { --color-hex3: #f00; --color-hex4: #F007; --color-hex6: #ff0000; --color-hex8: #ff000077; --color-named: black; --color-keyword: currentColor; }' },
{ label: '--color-hex8', kind: CompletionItemKind.Color, resultText: 'a { color: var(--color-hex8) } :root { --color-hex3: #f00; --color-hex4: #F007; --color-hex6: #ff0000; --color-hex8: #ff000077; --color-named: black; --color-keyword: currentColor; }' },
{ label: '--color-named', kind: CompletionItemKind.Color, resultText: 'a { color: var(--color-named) } :root { --color-hex3: #f00; --color-hex4: #F007; --color-hex6: #ff0000; --color-hex8: #ff000077; --color-named: black; --color-keyword: currentColor; }' },
{ label: '--color-keyword', kind: CompletionItemKind.Color, resultText: 'a { color: var(--color-keyword) } :root { --color-hex3: #f00; --color-hex4: #F007; --color-hex6: #ff0000; --color-hex8: #ff000077; --color-named: black; --color-keyword: currentColor; }' },
]
});
await testCompletionFor('a { color: | } :root { --border-hex3: solid #f00 1px; --border-hex4: solid #F007 1px; --border-hex6: 1px #ff0000 solid; --border-hex8: #ff000077 #ff000077; --border-named: solid black 1px; --border-keyword: currentColor wavy; }', {
items: [
{ label: '--border-hex3', kind: CompletionItemKind.Variable, resultText: 'a { color: var(--border-hex3) } :root { --border-hex3: solid #f00 1px; --border-hex4: solid #F007 1px; --border-hex6: 1px #ff0000 solid; --border-hex8: #ff000077 #ff000077; --border-named: solid black 1px; --border-keyword: currentColor wavy; }' },
{ label: '--border-hex4', kind: CompletionItemKind.Variable, resultText: 'a { color: var(--border-hex4) } :root { --border-hex3: solid #f00 1px; --border-hex4: solid #F007 1px; --border-hex6: 1px #ff0000 solid; --border-hex8: #ff000077 #ff000077; --border-named: solid black 1px; --border-keyword: currentColor wavy; }' },
{ label: '--border-hex6', kind: CompletionItemKind.Variable, resultText: 'a { color: var(--border-hex6) } :root { --border-hex3: solid #f00 1px; --border-hex4: solid #F007 1px; --border-hex6: 1px #ff0000 solid; --border-hex8: #ff000077 #ff000077; --border-named: solid black 1px; --border-keyword: currentColor wavy; }' },
{ label: '--border-hex8', kind: CompletionItemKind.Variable, resultText: 'a { color: var(--border-hex8) } :root { --border-hex3: solid #f00 1px; --border-hex4: solid #F007 1px; --border-hex6: 1px #ff0000 solid; --border-hex8: #ff000077 #ff000077; --border-named: solid black 1px; --border-keyword: currentColor wavy; }' },
{ label: '--border-named', kind: CompletionItemKind.Variable, resultText: 'a { color: var(--border-named) } :root { --border-hex3: solid #f00 1px; --border-hex4: solid #F007 1px; --border-hex6: 1px #ff0000 solid; --border-hex8: #ff000077 #ff000077; --border-named: solid black 1px; --border-keyword: currentColor wavy; }' },
{ label: '--border-keyword', kind: CompletionItemKind.Variable, resultText: 'a { color: var(--border-keyword) } :root { --border-hex3: solid #f00 1px; --border-hex4: solid #F007 1px; --border-hex6: 1px #ff0000 solid; --border-hex8: #ff000077 #ff000077; --border-named: solid black 1px; --border-keyword: currentColor wavy; }' },
]
});
await testCompletionFor('a { color: | } :root { --color-rgb: rgb(255 0 125 / 50%); }', {
items: [
{ label: '--color-rgb', kind: CompletionItemKind.Variable, resultText: 'a { color: var(--color-rgb) } :root { --color-rgb: rgb(255 0 125 / 50%); }' },
]
});
await testCompletionFor('a { color: | } :root { --border-rgb: solid rgb(255 0 125 / 50%) 2px; }', {
items: [
{ label: '--border-rgb', kind: CompletionItemKind.Variable, resultText: 'a { color: var(--border-rgb) } :root { --border-rgb: solid rgb(255 0 125 / 50%) 2px; }' },
]
});
});
test('support', async function () {
await testCompletionFor('@supports (display: flex) { |', {
Expand Down