diff --git a/client/modules/IDE/components/Editor/p5JavaScript.js b/client/modules/IDE/components/Editor/p5JavaScript.js index ebafb81eca..f3d0f2af5a 100644 --- a/client/modules/IDE/components/Editor/p5JavaScript.js +++ b/client/modules/IDE/components/Editor/p5JavaScript.js @@ -1,8 +1,57 @@ -import { LanguageSupport } from '@codemirror/language'; +import { LanguageSupport, syntaxTree } from '@codemirror/language'; import { javascript } from '@codemirror/lang-javascript'; +import { ViewPlugin, Decoration } from '@codemirror/view'; +import { RangeSetBuilder } from '@codemirror/state'; import { p5Hinter } from '../../../../utils/p5-hinter'; import { p5CompletionPreview } from './p5CompletionPreview'; import contextAwareHinter from '../../../../utils/contextAwareHinter'; +import { + p5FunctionKeywords, + p5VariableKeywords +} from '../../../../utils/p5-keywords'; + +const p5Functions = new Set(Object.keys(p5FunctionKeywords)); +const p5Variables = new Set(Object.keys(p5VariableKeywords)); + +const p5FunctionMark = Decoration.mark({ class: 'cm-p5-function' }); +const p5VariableMark = Decoration.mark({ class: 'cm-p5-variable' }); + +function buildDecorations(view) { + const builder = new RangeSetBuilder(); + view.visibleRanges.forEach(({ from, to }) => { + syntaxTree(view.state).iterate({ + from, + to, + enter(node) { + const isVariable = node.name === 'VariableName'; + const isDefinition = node.name === 'VariableDefinition'; + if (!isVariable && !isDefinition) return; + const name = view.state.doc.sliceString(node.from, node.to); + if (p5Functions.has(name)) { + builder.add(node.from, node.to, p5FunctionMark); + } else if (p5Variables.has(name)) { + builder.add(node.from, node.to, p5VariableMark); + } + } + }); + }); + return builder.finish(); +} + +const p5Highlight = ViewPlugin.fromClass( + class { + constructor(view) { + this.decorations = buildDecorations(view); + } + + update(update) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildDecorations(update.view); + } + } + }, + { decorations: (v) => v.decorations } +); function addCompletions(context) { const word = context.matchBefore(/\w*/); @@ -23,6 +72,7 @@ export default function p5JavaScript() { jsLang.language.data.of({ autocomplete: addCompletions }), - p5CompletionPreview() + p5CompletionPreview(), + p5Highlight ]); } diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index 93866036cd..72051b096a 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -306,7 +306,22 @@ // contentSeparator, monospace, strikethrough, inserted, deleted, changed, invalid, meta, // documentMeta, annotation, processingInstruction, standard, special, macroName -// TODO(connie): Add p5 specific highlighting styles, like .cm-p5-function, .cm-p5-variable +// CM6 uses EditorView.theme() instead of cm-s-* classes, so the p5 highlight styles +// from the CM5 theme files do not apply. These rules replicate them for CM6. +.cm-p5-function, +.cm-p5-function .cm-variable { + @include themify() { + color: getThemifyVariable('highlight-style-variable-color'); + font-weight: bold; + } +} + +.cm-p5-variable, +.cm-p5-variable .cm-variable { + @include themify() { + color: getThemifyVariable('highlight-style-atom-color'); + } +} // Additional matching selection styling .cm-matchingBracket {