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

Playground autocomplete #77

Closed
wants to merge 7 commits into from
Closed
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
311 changes: 311 additions & 0 deletions source/assets/js/playground/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
import {
CompletionContext,
CompletionResult,
CompletionSource,
} from '@codemirror/autocomplete';
import {sassCompletionSource} from '@codemirror/lang-sass';
import {syntaxTree} from '@codemirror/language';
import {EditorState} from '@codemirror/state';

// Sass-specific at rules only. CSS at rules should be added to `@codemirror/lang-css`.
const atRuleKeywords = [
'use',
'forward',
'import',
'mixin',
'include',
'function',
'extend',
'error',
'warn',
'debug',
'at-root',
'if',
'else',
'each',
'for',
'while',
];
Comment on lines +11 to +28
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to add @media, @supports, and @keyframes here? And maybe also add autocomplete for other CSS at-rules?

Copy link
Member

Choose a reason for hiding this comment

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

I think css at-rules would be good to consider supporting. Hopefully there's a clear enough division between that and auto-completing css properties/values, so this doesn't become a very slippery scope slope.

Copy link
Author

Choose a reason for hiding this comment

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

I think that probably should go upstream into @codemirror/lang-css- that shouldn't be too challenging, I don't think.

We are already getting auto-completion of css properties and values from that package.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense -- add a comment or file a new issue so we don't lose track?


const atRuleOptions = Object.freeze(
atRuleKeywords.map(keyword => ({
label: `@${keyword} `,
type: 'keyword',
}))
);

function atRuleCompletion(context: CompletionContext): CompletionResult | null {
const atRule = context.matchBefore(/@\w*/);
if (!atRule) return null;
if (atRule.from === atRule.to && !context.explicit) return null;
return {
from: atRule.from,
to: atRule.to,
options: atRuleOptions,
};
}

type CompletionInfo = {
name: string;
description?: string;
};

type ModuleDefinition = CompletionInfo & {
functions?: CompletionInfo[];
variables?: CompletionInfo[];
};

const builtinModules: ModuleDefinition[] = [
{
name: 'color',
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason we're not including the color functions (e.g. adjust, scale, etc) here?

Copy link
Author

Choose a reason for hiding this comment

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

I'm holding out hope there's a way to get this from Sass directly, as this is a long list, and will be changing once the CSS Color 4 changes are made in Sass.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense -- add a comment so we don't forget?

description:
'generates new colors based on existing ones, making it easy to build color themes',
// todo: Add functions after Color 4 updates, or see if list can be generated from Sass package.
},
{
name: 'list',
description: 'lets you access and modify values in lists',
functions: [
{name: 'length'},
{name: 'nth'},
{name: 'set-nth'},
{name: 'join'},
{name: 'append'},
{name: 'zip'},
{name: 'index'},
{name: 'is-bracketed'},
{name: 'separator'},
{name: 'slash'},
],
},
{
name: 'map',
description:
'makes it possible to look up the value associated with a key in a map, and much more',
functions: [
{name: 'get'},
{name: 'set'},
{name: 'merge'},
{name: 'remove'},
{name: 'keys'},
{name: 'values'},
{name: 'has-key'},
{name: 'deep-merge'},
{name: 'deep-remove'},
],
},
{
name: 'math',
description: 'provides functions that operate on numbers',
variables: [
{name: '$e'},
{name: '$pi'},
{name: '$epsilon'},
{name: '$max-safe-integer'},
{name: '$min-safe-integer'},
{name: '$max-number'},
{name: '$min-number'},
],
functions: [
{name: 'ceil'},
{name: 'clamp'},
{name: 'floor'},
{name: 'max'},
{name: 'min'},
{name: 'round'},
{name: 'abs'},
{name: 'hypot'},
{name: 'log'},
{name: 'pow'},
{name: 'sqrt'},
{name: 'acos'},
{name: 'asin'},
{name: 'atan'},
{name: 'atan2'},
{name: 'cos'},
{name: 'sin'},
{name: 'tan'},
{name: 'compatible'},
{name: 'is-unitless'},
{name: 'unit'},
{name: 'div'},
{name: 'percentage'},
{name: 'random'},
],
},
{
name: 'meta',
description: 'exposes the details of Sass’s inner workings',
functions: [
{name: 'apply'},
{name: 'load-css'},
{name: 'accepts-content'},
{name: 'calc-args'},
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
{name: 'calc-name'},
{name: 'call'},
{name: 'context-exists'},
{name: 'feature-exists'},
{name: 'function-exists'},
{name: 'mixin-exists'},
{name: 'variable-exists'},
{name: 'get-function'},
{name: 'get-mixin'},
{name: 'global-variable-exists'},
{name: 'keywords'},
{name: 'module-functions'},
{name: 'module-mixins'},
{name: 'module-variables'},
{name: 'type-of'},
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
{name: 'inspect'},
],
},
{
name: 'selector',
description: 'provides access to Sass’s powerful selector engine',
functions: [
{name: 'is-superselector'},
{name: 'simple-selectors'},
{name: 'parse'},
{name: 'nest'},
{name: 'append'},
{name: 'extend'},
{name: 'replace'},
{name: 'unify'},
],
},
{
name: 'string',
description: 'makes it easy to combine, search, or split apart strings',
functions: [
{name: 'unquote'},
{name: 'quote'},
{name: 'to-upper-case'},
{name: 'to-lower-case'},
{name: 'length'},
{name: 'insert'},
{name: 'index'},
{name: 'slice'},
{name: 'unique-id'},
{name: 'split'},
],
},
];

const moduleNames = builtinModules.map(mod => mod.name);
const moduleNameRegExp = new RegExp(`(${moduleNames.join('|')}).\\$?\\w*`);

const moduleCompletions = Object.freeze(
builtinModules.map(mod => ({
label: `sass:${mod.name}`,
apply: `sass:${mod.name}`,
info: mod.description,
type: 'class',
}))
);

function builtinModulesCompletion(
context: CompletionContext
): CompletionResult | null {
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
const potentialTypes = [
'StringLiteral', // When wrapped in quotes- `"sass:"`
'ValueName', // No end quote, before semicolon- `"sa`
':', // No end quote, on semicolon- `"sass:`
'PseudoClassName', // No end quote, after semicolon- `"sass:m`
]
if (!potentialTypes.includes(nodeBefore.type.name)) return null;
const potentialParentTypes = [
'UseStatement', // When wrapped in quotes
'PseudoClassSelector', // No end quote, after the colon
]
if (!potentialParentTypes.includes(nodeBefore.parent?.type.name || '')) return null;

const moduleMatch = context.matchBefore(/["'](sass:)?\w*/);

if (!moduleMatch) return null;
if (moduleMatch.from === moduleMatch.to && !context.explicit) return null;
return {
from: moduleMatch.from + 1,
to: moduleMatch.to,
options: moduleCompletions,
};
}

const moduleVariableCompletions = Object.freeze(
builtinModules.reduce(
(acc: {[k: string]: CompletionResult['options'] | []}, mod) => {
acc[mod.name] =
mod.variables?.map(variable => ({
label: `${mod.name}.${variable.name}`,
info: variable?.description,
type: 'variable',
})) || [];
return acc;
},
{}
)
);

const moduleFunctionsCompletions = Object.freeze(
builtinModules.reduce(
(acc: {[k: string]: CompletionResult['options'] | []}, mod) => {
acc[mod.name] =
mod.functions?.map(variable => ({
label: `${mod.name}.${variable.name}`,
apply: `${mod.name}.${variable.name}(`,
info: variable?.description,
type: 'method',
boost: 10,
})) || [];
return acc;
},
{}
)
);

function includedBuiltinModules(state: EditorState) {
const text = state.doc.toString();
const modNames = builtinModules.map(mod => mod.name);
return modNames.filter(name => text.includes(`sass:${name}`));
}

function builtinModuleItemCompletion(
context: CompletionContext
): CompletionResult | null {
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
if (
![nodeBefore.type.name, nodeBefore.parent?.type.name].includes(
'NamespacedValue'
)
)
return null;
const moduleNameMatch = context.matchBefore(moduleNameRegExp);

if (!moduleNameMatch) return null;
if (moduleNameMatch.from === moduleNameMatch.to && !context.explicit)
return null;

const includedModules = includedBuiltinModules(context.state);

const includedModFunctions = includedModules.flatMap(
mod => moduleFunctionsCompletions[mod]
);
const includedModVariables = includedModules.flatMap(
mod => moduleVariableCompletions[mod]
);

return {
from: moduleNameMatch.from,
to: moduleNameMatch.to,
options: [...includedModVariables, ...includedModFunctions],
};
}

const playgroundCompletions: CompletionSource[] = [
atRuleCompletion,
builtinModulesCompletion,
sassCompletionSource,
builtinModuleItemCompletion,
];

export default playgroundCompletions;
3 changes: 2 additions & 1 deletion source/assets/js/playground/editor-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from '@codemirror/view';

import {playgroundHighlightStyle} from './theme.js';
import playgroundCompletions from './autocomplete.js';

const editorSetup = (() => [
[
Expand All @@ -48,7 +49,7 @@ const editorSetup = (() => [
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
bracketMatching(),
closeBrackets(),
autocompletion(),
autocompletion({override: playgroundCompletions}),
highlightActiveLine(),
drawSelection(),
keymap.of([
Expand Down