Skip to content
Draft
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"ts-pnp": "1.2.0",
"url": "^0.11.4",
"watch": "^1.0.2",
"web-tree-sitter": "0.25.9",
"web-vitals": "^1.0.1"
},
"scripts": {
Expand Down
Binary file added public/wasm/tree-sitter-python.wasm
Binary file not shown.
Binary file added public/wasm/tree-sitter.wasm
Binary file not shown.
109 changes: 109 additions & 0 deletions src/assets/stylesheets/EditorPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,112 @@
.rpf-alert {
margin: 0;
}


// tree sitter
.problem-marker {
position: relative;
cursor: help;

&::before {
content: attr(data-error-type);
position: absolute;
display: none;
z-index: 9999;
}

&::after {
content: attr(data-message);
position: absolute; /* Changed from fixed to absolute for proper positioning */
display: none;
top: 100%; /* Position below the marker by default */
left: 0; /* Align with the left of the marker */
background: #fff;
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 9999;
white-space: pre-wrap;
min-width: 200px;
max-width: 400px;
font-family: sans-serif;
font-size: 13px;
color: #333;
pointer-events: none;
overflow: visible;
max-height: none;
border-left: 4px solid #e91e63;
transform: translateY(5px); /* Small offset for better visibility */
}

&:hover::after,
&:focus::after {
display: block;
}

/* Position tooltip based on available space */
&[data-line-position="top"]::after {
top: auto; /* Reset top positioning */
bottom: 100%; /* Position above instead of below */
transform: translateY(-5px); /* Offset upwards */
}
}

.problem-error {
border-bottom: 2px dotted #ff4b4b;
}

.problem-warning {
border-bottom: 2px dotted #ffb74d;
}

/* Support for touch devices */
@media (pointer: coarse) {
.problem-marker {
&::after {
display: none;
}

&:active::after {
display: block;
}
}
}

// Tooltip styling
.cm-tooltip {
max-width: 300px;
white-space: pre-wrap;
z-index: 1000;
}

.cm-tooltip .problem-tooltip {
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
padding: 8px;
font-size: 13px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

// Loading indicator
.parser-loading {
padding: 10px;
background: #fff3cd;
color: #856404;
margin: 10px 0;
border-radius: 4px;
border: 1px solid #ffeaa7;
}

// Python parser loader
.python-parser-loader {
padding: 10px;
background: #fff3cd;
color: #856404;
margin: 10px 0;
border-radius: 4px;
text-align: center;
font-weight: 500;
}
101 changes: 97 additions & 4 deletions src/components/Editor/EditorPanel/EditorPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { useCookies } from "react-cookie";
import { useTranslation } from "react-i18next";
import { basicSetup } from "codemirror";
import { EditorView, keymap } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import { EditorState, StateField } from "@codemirror/state";
import { Decoration } from "@codemirror/view";
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
import { indentationMarkers } from "@replit/codemirror-indentation-markers";
import { indentUnit } from "@codemirror/language";
Expand All @@ -25,6 +26,7 @@ import { Alert } from "@raspberrypifoundation/design-system-react";
import { editorLightTheme } from "../../../assets/themes/editorLightTheme";
import { editorDarkTheme } from "../../../assets/themes/editorDarkTheme";
import { SettingsContext } from "../../../utils/settings";
import useTreeSitterParser from "../../../hooks/useTreeSitterParser";

const MAX_CHARACTERS = 8500000;

Expand All @@ -40,6 +42,34 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => {
const settings = useContext(SettingsContext);
const [characterLimitExceeded, setCharacterLimitExceeded] = useState(false);

// Use the new Tree Sitter hook
const {
treeSitterParser,
parserInitialized,
analyzePythonCode,
createProblemDecorations,
problemDecorationsEffect,
} = useTreeSitterParser(extension, fileName);

// Create a state field for problem decorations
const problemDecorationsField = StateField.define({
create() {
return Decoration.none;
},
update(decorations, transaction) {
decorations = decorations.map(transaction.changes);

for (let effect of transaction.effects) {
if (effect.is(problemDecorationsEffect)) {
decorations = effect.value;
}
}

return decorations;
},
provide: (f) => EditorView.decorations.from(f),
});

const updateStoredProject = (content) => {
dispatch(
updateProjectComponent({
Expand All @@ -54,9 +84,34 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => {
const label = EditorView.contentAttributes.of({
"aria-label": t("editorPanel.ariaLabel"),
});

const onUpdate = EditorView.updateListener.of((viewUpdate) => {
if (viewUpdate.docChanged) {
updateStoredProject(viewUpdate.state.doc.toString());
const content = viewUpdate.state.doc.toString();
updateStoredProject(content);

// Analyze Python code if applicable
if (extension === "py" && treeSitterParser) {
analyzePythonCode(content).then((problems) => {
// Create decorations for problem areas
if (editorViewRef.current && problems.length > 0) {
const decorationSet = createProblemDecorations(
viewUpdate.state,
problems,
);

// Update the editor view with decorations
editorViewRef.current.dispatch({
effects: [problemDecorationsEffect.of(decorationSet)],
});
} else if (editorViewRef.current) {
// Clear decorations if no problems
editorViewRef.current.dispatch({
effects: [problemDecorationsEffect.of(Decoration.none)],
});
}
});
}
}
});

Expand All @@ -74,6 +129,7 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => {
return html();
}
};

const isDarkMode =
cookies.theme === "dark" ||
(!cookies.theme &&
Expand Down Expand Up @@ -120,6 +176,7 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => {
indentUnit.of(customIndentUnit),
EditorView.editable.of(!readOnly),
limitCharacters,
problemDecorationsField,
],
});

Expand All @@ -141,10 +198,22 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => {
img.setAttribute("role", "presentation");
}

// Initial analysis for Python files when the editor is first created
if (extension === "py" && treeSitterParser && parserInitialized) {
analyzePythonCode(code).then((problems) => {
if (problems.length > 0 && view) {
const decorationSet = createProblemDecorations(view.state, problems);
view.dispatch({
effects: [problemDecorationsEffect.of(decorationSet)],
});
}
});
}

return () => {
view.destroy();
};
}, [cookies]);
}, [cookies, treeSitterParser, parserInitialized]);

useEffect(() => {
if (
Expand All @@ -160,12 +229,36 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => {
},
});
dispatch(setCascadeUpdate(false));

// Re-analyze Python code after cascade update
if (extension === "py" && treeSitterParser) {
analyzePythonCode(file.content).then((problems) => {
if (problems.length > 0) {
const decorationSet = createProblemDecorations(
editorViewRef.current.state,
problems,
);
editorViewRef.current.dispatch({
effects: [problemDecorationsEffect.of(decorationSet)],
});
} else {
editorViewRef.current.dispatch({
effects: [problemDecorationsEffect.of(Decoration.none)],
});
}
});
}
}
}, [file, cascadeUpdate, editorViewRef]);
}, [file, cascadeUpdate, editorViewRef, treeSitterParser, extension]);

return (
<div className="editor-wrapper">
<div className={`editor editor--${settings.fontSize}`} ref={editor}></div>
{extension === "py" && !parserInitialized && (
<div className="python-parser-loader">
{t("editorPanel.pythonSyntaxErrors.loadingParser")}
</div>
)}
{characterLimitExceeded && (
<Alert
title={t("editorPanel.characterLimitError")}
Expand Down
Loading
Loading