From 2488fedba6bd5e8997cdc9967bfe7e30be08ff20 Mon Sep 17 00:00:00 2001 From: nahbee10 Date: Mon, 30 Sep 2024 22:11:24 -0400 Subject: [PATCH] fix init editor view using hint functions --- .../modules/IDE/components/Editor/index.jsx | 303 +++++++++++++++++- package-lock.json | 5 +- package.json | 1 + 3 files changed, 291 insertions(+), 18 deletions(-) diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx index d5102b9a0..7f45f3fb6 100644 --- a/client/modules/IDE/components/Editor/index.jsx +++ b/client/modules/IDE/components/Editor/index.jsx @@ -16,6 +16,7 @@ import { lineNumbers } from '@codemirror/view'; import { javascript } from '@codemirror/lang-javascript'; +import { css } from '@codemirror/lang-css'; import { bracketMatching, foldGutter, foldKeymap } from '@codemirror/language'; import { autocompletion, closeBrackets } from '@codemirror/autocomplete'; import { linter, lintGutter } from '@codemirror/lint'; @@ -50,6 +51,7 @@ import Pickr from '@simonwep/pickr'; import classNames from 'classnames'; import StackTrace from 'stacktrace-js'; +import beepUrl from '../../../../sounds/audioAlert.mp3'; import RightArrowIcon from '../../../../images/right-arrow.svg'; import LeftArrowIcon from '../../../../images/left-arrow.svg'; import { metaKey } from '../../../../utils/metaKey'; @@ -74,6 +76,10 @@ import * as ConsoleActions from '../../actions/console'; const INDENTATION_AMOUNT = 2; +window.JSHINT = JSHINT; +window.CSSLint = CSSLint; +window.HTMLHint = HTMLHint; + const getFileMode = (fileName) => { if (fileName.match(/.+\.js$/i)) return 'javascript'; if (fileName.match(/.+\.css$/i)) return 'css'; @@ -116,9 +122,38 @@ const Editor = (props) => { t, collapseSidebar, expandSidebar, - lintMessages + lintMessages, + clearLintMessage, + updateLintMessage, + lintWarning, + autocompleteHinter } = props; + const beepRef = useRef(null); + + useEffect(() => { + beepRef.current = new Audio(beepUrl); + + return () => { + beepRef.current = null; + }; + }, [beepUrl]); + + const updateLintingMessageAccessibility = useCallback( + debounce((annotations) => { + clearLintMessage(); + annotations.forEach((x) => { + if (x.from.line > -1) { + updateLintMessage(x.severity, x.from.line + 1, x.message); + } + }); + if (lintMessages.length > 0 && lintWarning) { + beepRef.current.play(); + } + }, 2000), + [] + ); + const editorRef = useRef(null); const viewRef = useRef(null); const pickrRef = useRef(null); @@ -177,13 +212,6 @@ const Editor = (props) => { setCurrentLine(lineNumber); }, []); - const triggerFindPersistent = () => { - const view = viewRef.current; - if (view) { - view.dispatch({ effects: find.of('') }); // Dispatch the find effect - } - }; - // Initialize Pickr color picker const initColorPicker = () => { const pickr = Pickr.create({ @@ -221,6 +249,36 @@ const Editor = (props) => { pickrRef.current = pickr; }; + const myLinterFunction = (view) => { + const diagnostics = []; + const content = view.state.doc.toString(); // Get the content of the editor + + // Pass the content through JSHint (or any other linter) + JSHINT(content, { + asi: true, // Allow missing semicolons + eqeqeq: false, // Allow non-strict equality (== and !=) + '-W041': false, // Disable warning for 'use of == null' + esversion: 11 // Use ECMAScript version 11 (ES2020) + }); + + // Process JSHint results and convert them into CodeMirror diagnostics + JSHINT.errors.forEach((error) => { + if (!error) return; + + diagnostics.push({ + from: view.state.doc.line(error.line).from + (error.character - 1), // Position of the error + to: view.state.doc.line(error.line).from + error.character, // End position of the error + severity: error.code.startsWith('W') ? 'warning' : 'error', // 'W' indicates a warning in JSHint + message: error.reason // The error message + }); + }); + + // Call the onUpdateLinting function to handle linting messages + updateLintingMessageAccessibility(diagnostics); + + return diagnostics; + }; + // Open the color picker when `MetaKey + K` is pressed const openColorPicker = () => { if (pickrRef.current) { @@ -228,6 +286,13 @@ const Editor = (props) => { } }; + const triggerFindPersistent = () => { + const view = viewRef.current; + if (view) { + view.dispatch({ effects: find.of('') }); // Dispatch the find effect + } + }; + const customKeymap = [ { key: 'Tab', @@ -292,15 +357,117 @@ const Editor = (props) => { ...standardKeymap // Default key bindings from CodeMirror ]; + let focusedLinkElement = null; + useEffect(() => { + if (!editorRef.current) return; + initColorPicker(); + const setFocusedLinkElement = (set) => { + if (set && !focusedLinkElement) { + const activeItemLink = document.querySelector( + '.cm-tooltip-autocomplete a' + ); + if (activeItemLink) { + focusedLinkElement = activeItemLink; + focusedLinkElement.classList.add('focused-hint-link'); + activeItemLink.parentElement?.parentElement?.classList.add( + 'unfocused' + ); + } + } + }; + + const removeFocusedLinkElement = () => { + if (focusedLinkElement) { + focusedLinkElement.classList.remove('focused-hint-link'); + focusedLinkElement.parentElement?.parentElement?.classList.remove( + 'unfocused' + ); + focusedLinkElement = null; + return true; + } + return false; + }; + + const javascriptCompletion = (context) => { + const token = context.matchBefore(/\w*/); + if (!token) return null; + + const hints = hinter + .search(token.text) + .filter((h) => h.item.text[0] === token.text[0]); + + return { + from: token.from, + options: hints.map((h) => ({ + label: h.item.text, + apply: h.item.text + })) + }; + }; + + // const updateContentDebounced = lodashDebounce(() => { + // setUnsavedChanges(true); + // hideRuntimeErrorWarning(); + // if (view) { + // const content = view.state.doc.toString(); + // updateFileContent(file.id, content); + // if (autorefresh && isPlaying) { + // clearConsole(); + // startSketch(); + // } + // } + // }, 1000); + + const hintExtension = autocompletion({ + override: [javascriptCompletion], // Use the javascript completion we defined earlier + closeOnUnfocus: false, + extraKeys: { + 'Shift-Right': () => { + const activeItemLink = document.querySelector( + '.cm-tooltip-autocomplete a' + ); + if (activeItemLink) activeItemLink.click(); + return true; + }, + Right: () => { + setFocusedLinkElement(true); + return true; + }, + Left: () => { + removeFocusedLinkElement(); + return true; + }, + Up: () => { + const onLink = removeFocusedLinkElement(); + // Move focus manually here if needed + setFocusedLinkElement(onLink); + return true; + }, + Down: () => { + const onLink = removeFocusedLinkElement(); + // Move focus manually here if needed + setFocusedLinkElement(onLink); + return true; + }, + Enter: () => { + if (focusedLinkElement) { + focusedLinkElement.click(); + return true; + } + return false; + } + } + }); + const startState = EditorState.create({ doc: file.content, extensions: [ javascript(), autocompletion(), - linter(() => []), // Linter extension placeholder + linter(myLinterFunction), // Linter extension placeholder lintGutter(), abbreviationTracker(), lineNumbers(), // Line numbers @@ -310,6 +477,8 @@ const Editor = (props) => { bracketMatching(), // Match brackets closeBrackets(), // Automatically close brackets highlightSelectionMatches(), // Highlight search matches + css(), + hintExtension, keymap.of(customKeymap), EditorView.updateListener.of((update) => { if (update.docChanged) { @@ -325,6 +494,57 @@ const Editor = (props) => { }); viewRef.current = view; + const showHint = () => { + if (view) { + // Trigger hinting manually if needed; CM6 autocompletion happens automatically with the proper extensions + view.dispatch({ + effects: autocompletion.startCompletion.of({ + source: javascriptCompletion + }) + }); + } + }; + + const onKeyDown = (e) => { + const mode = view?.state.facet(EditorState.languageDataAt)?.[0]; + if (/^[a-z]$/i.test(e.key) && (mode === 'css' || mode === 'javascript')) { + showHint(); + } + if (e.key === 'Escape') { + e.preventDefault(); + e.target.blur(); + } + }; + + // const state = EditorState.create({ + // doc: file.content, + // extensions: [ + // basicSetup, + // javascript(), + // css(), + // hintExtension, + // EditorView.updateListener.of((update) => { + // if (update.docChanged) { + // updateContentDebounced(); // Debounce the changes + // } + // }), + // keymap.of([ + // { + // key: 'Enter', + // run: (view) => { + // showHint(); + // return true; + // } + // } + // ]) + // ] + // }); + + view.dom.style.fontSize = `${fontSize}px`; // Apply font size + + // Attach keydown handler + view.dom.addEventListener('keydown', onKeyDown); + provideController({ tidyCode, showFind, @@ -332,9 +552,64 @@ const Editor = (props) => { getContent }); - return () => view.destroy(); + // eslint-disable-next-line consistent-return + return () => { + view.dom.removeEventListener('keydown', onKeyDown); + view.destroy(); + }; }, []); + // useEffect(() => { + // initColorPicker(); + + // const startState = EditorState.create({ + // doc: file.content, + // extensions: [ + // javascript(), + // autocompletion(), + // linter(myLinterFunction), // Linter extension placeholder + // lintGutter(), + // abbreviationTracker(), + // lineNumbers(), // Line numbers + // highlightActiveLine(), // Highlight active line + // EditorView.lineWrapping, // Line wrapping + // foldGutter(), // Fold gutter + // bracketMatching(), // Match brackets + // closeBrackets(), // Automatically close brackets + // highlightSelectionMatches(), // Highlight search matches + // keymap.of(customKeymap), + // EditorView.updateListener.of((update) => { + // if (update.docChanged) { + // handleEditorChange(); // Ensure changes are handled properly + // } + // }) + // ] + // }); + + // const view = new EditorView({ + // state: startState, + // parent: editorRef.current + // }); + // viewRef.current = view; + + // view.dom.style.fontSize = `${fontSize}px`; // Apply font size + + // // Attach keydown handler + // view.dom.addEventListener('keydown', onKeyDown); + + // provideController({ + // tidyCode, + // showFind, + // showReplace, + // getContent + // }); + + // return () => { + // view.dom.removeEventListener('keydown', onKeyDown); + // view.destroy(); + // }; + // }, []); + return (
{ Editor.propTypes = { // autocloseBracketsQuotes: PropTypes.bool.isRequired, - // autocompleteHinter: PropTypes.bool.isRequired, + autocompleteHinter: PropTypes.bool.isRequired, // lineNumbers: PropTypes.bool.isRequired, - // lintWarning: PropTypes.bool.isRequired, + lintWarning: PropTypes.bool.isRequired, // linewrap: PropTypes.bool.isRequired, lintMessages: PropTypes.arrayOf( PropTypes.shape({ @@ -399,8 +674,8 @@ Editor.propTypes = { // args: PropTypes.arrayOf(PropTypes.string) // }) // ).isRequired, - // updateLintMessage: PropTypes.func.isRequired, - // clearLintMessage: PropTypes.func.isRequired, + updateLintMessage: PropTypes.func.isRequired, + clearLintMessage: PropTypes.func.isRequired, updateFileContent: PropTypes.func.isRequired, fontSize: PropTypes.number.isRequired, file: PropTypes.shape({ diff --git a/package-lock.json b/package-lock.json index 954c22ada..6ddd3a0c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@babel/register": "^7.14.5", "@codemirror/autocomplete": "^6.18.0", "@codemirror/commands": "^6.6.1", + "@codemirror/lang-css": "^6.3.0", "@codemirror/lang-javascript": "^6.2.2", "@codemirror/language": "^6.10.3", "@codemirror/lint": "^6.8.1", @@ -6130,7 +6131,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz", "integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -8386,7 +8386,6 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.8.tgz", "integrity": "sha512-7JhxupKuMBaWQKjQoLtzhGj83DdnZY9MckEOG5+/iLKNK2ZJqKc6hf6uc0HjwCX7Qlok44jBNqZhHKDhEhZYLA==", - "peer": true, "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", @@ -46229,7 +46228,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz", "integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==", - "peer": true, "requires": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -47991,7 +47989,6 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.8.tgz", "integrity": "sha512-7JhxupKuMBaWQKjQoLtzhGj83DdnZY9MckEOG5+/iLKNK2ZJqKc6hf6uc0HjwCX7Qlok44jBNqZhHKDhEhZYLA==", - "peer": true, "requires": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", diff --git a/package.json b/package.json index 51e188b8a..04186601b 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,7 @@ "@babel/register": "^7.14.5", "@codemirror/autocomplete": "^6.18.0", "@codemirror/commands": "^6.6.1", + "@codemirror/lang-css": "^6.3.0", "@codemirror/lang-javascript": "^6.2.2", "@codemirror/language": "^6.10.3", "@codemirror/lint": "^6.8.1",