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

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',
},
{
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', description: undefined},
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
{name: '$pi'},
{name: '$epsilon'},
{name: '$max-safe-integer'},
{name: '$min-safe-integer'},
{name: '$max-number'},
{name: '$min-number'},
],
functions: [
{name: 'ceil', description: undefined},
{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: '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: 'selector',
description: 'provides access to Sass’s powerful selector engine',
functions: [
{name: 'isSuperselector'},
{name: 'simpleSelectors'},
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
{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)*`);
jamesnw marked this conversation as resolved.
Show resolved Hide resolved

const moduleCompletions = Object.freeze(
builtinModules.map(mod => ({
label: `"sass:${mod.name}"`,
// don't add extra quote on the end, as it likely is already there
apply: `"sass:${mod.name}`,
info: mod.description,
type: 'class',
}))
);

function builtinModulesCompletion(
context: CompletionContext
): CompletionResult | null {
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
if (nodeBefore.parent?.type.name !== 'UseStatement') return null;

const atRule = context.matchBefore(/"(sass:)?\w*/);

if (!atRule) return null;
if (atRule.from === atRule.to && !context.explicit) return null;
return {
from: atRule.from,
to: atRule.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 atRule = context.matchBefore(moduleNameRegExp);

if (!atRule) return null;
if (atRule.from === atRule.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: atRule.from,
to: atRule.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