-
-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from all commits
4d72eb7
f5dbee6
65f116f
cc11eff
b58c1bb
b699ec7
b2937f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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', | ||
]; | ||
|
||
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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason we're not including the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
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.
Do we want to add
@media
,@supports
, and@keyframes
here? And maybe also add autocomplete for other CSS at-rules?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 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.
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 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.
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.
Makes sense -- add a comment or file a new issue so we don't lose track?