Skip to content

Commit 1357e69

Browse files
authored
Merge pull request #489 from makermelissa-piclaw/feature/issue-361-language-color-coding
Add syntax highlighting for additional file types
2 parents 88e9202 + 4179361 commit 1357e69

4 files changed

Lines changed: 334 additions & 24 deletions

File tree

js/script.js

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { basicSetup } from "codemirror";
22
import { EditorView, keymap } from "@codemirror/view";
3-
import { EditorState } from "@codemirror/state";
3+
import { EditorState, Compartment } from "@codemirror/state";
44
import { indentWithTab } from "@codemirror/commands"
55
import { python } from "@codemirror/lang-python";
6+
import { json } from "@codemirror/lang-json";
7+
import { html } from "@codemirror/lang-html";
8+
import { css } from "@codemirror/lang-css";
9+
import { javascript } from "@codemirror/lang-javascript";
10+
import { xml } from "@codemirror/lang-xml";
11+
import { markdown } from "@codemirror/lang-markdown";
612
import { syntaxHighlighting, indentUnit } from "@codemirror/language";
713
import { classHighlighter } from "@lezer/highlight";
814
import { circuitpythonHighlight } from "./common/circuitpython_highlight.js";
@@ -56,6 +62,87 @@ const settings = new Settings();
5662

5763
const editorTheme = EditorView.theme({}, {dark: getCssVar('editor-theme-dark').trim() === '1'});
5864

65+
// Map file extensions to a CodeMirror 6 language extension factory.
66+
// Anything not in this map falls back to plain text (no language plugin).
67+
// Python is handled separately because it also gets the CircuitPython
68+
// highlight overlay.
69+
const LANGUAGE_EXTENSION_MAP = {
70+
"css": css,
71+
"htm": html,
72+
"html": html,
73+
"js": javascript,
74+
"json": json,
75+
"md": markdown,
76+
"xml": xml,
77+
};
78+
79+
function getFileExtensionFromPath(path) {
80+
if (!path) return null;
81+
// Use the basename so a dotted directory in the path doesn't fool us.
82+
const base = path.split("/").pop();
83+
if (!base || base.indexOf(".") < 0) return null;
84+
return base.split(".").pop().toLowerCase();
85+
}
86+
87+
// Pick the CodeMirror language extensions to use for a given file path.
88+
// Returns an array so callers can spread it directly into the editor's
89+
// extension list. New (untitled) docs default to Python so the editor
90+
// behaves the same as before for the common "create code.py" case.
91+
function languageExtensionsForPath(path) {
92+
if (path === null || path === undefined) {
93+
return [python(), circuitpythonHighlight];
94+
}
95+
const ext = getFileExtensionFromPath(path);
96+
if (ext === "py") {
97+
return [python(), circuitpythonHighlight];
98+
}
99+
if (ext && Object.prototype.hasOwnProperty.call(LANGUAGE_EXTENSION_MAP, ext)) {
100+
return [LANGUAGE_EXTENSION_MAP[ext]()];
101+
}
102+
return [];
103+
}
104+
105+
// Compartment used so we can hot-swap the language plugin when the
106+
// active file's extension changes (e.g. user opens an .html file, or
107+
// uses Save As to rename code.py to test.html).
108+
const languageCompartment = new Compartment();
109+
110+
// Track which path the editor's language plugin is currently configured
111+
// for, so we can decide whether a reconfigure is actually needed. We
112+
// can't compare against `workflow.currentFilename` because
113+
// `workflow.saveFileAs()` mutates that BEFORE the post-save
114+
// `setFilename` callback runs, so by the time we'd see it the
115+
// "old" path is already gone.
116+
let editorLanguagePath = null;
117+
118+
function extensionKey(path) {
119+
if (path === null || path === undefined) return "__null__";
120+
const ext = getFileExtensionFromPath(path);
121+
return ext || "__noext__";
122+
}
123+
124+
// Apply the language plugin matching `path` to the running editor.
125+
// Safe to call before `editor` exists (the initial state already gets
126+
// the correct language via languageCompartment.of(...) below).
127+
function setEditorLanguageForPath(path) {
128+
if (!editor) {
129+
editorLanguagePath = path;
130+
return;
131+
}
132+
if (extensionKey(path) === extensionKey(editorLanguagePath)) {
133+
// Same language plugin would be installed — skip the
134+
// reconfigure to avoid needlessly resetting language-internal
135+
// state (folds, parser caches, etc.).
136+
return;
137+
}
138+
editorLanguagePath = path;
139+
editor.dispatch({
140+
effects: languageCompartment.reconfigure(
141+
languageExtensionsForPath(path),
142+
),
143+
});
144+
}
145+
59146
document.addEventListener('DOMContentLoaded', function() {
60147
document.getElementById('mobile-menu-button').addEventListener('click', handleMobileToggle);
61148
document.querySelectorAll('#mobile-menu-contents li a').forEach((element) => {
@@ -279,6 +366,12 @@ async function checkReadOnly() {
279366

280367
/* Update the filename and update the UI */
281368
function setFilename(path) {
369+
// Refresh the CodeMirror language plugin whenever the active file
370+
// changes — this is the single chokepoint that all filename
371+
// changes route through (Open File, New File, Save As, backend
372+
// load), so it's the right place to keep the language in sync.
373+
setEditorLanguageForPath(path);
374+
282375
// Use the extension_map to figure out the file icon
283376
let filename = path;
284377

@@ -398,29 +491,42 @@ const hotkeyMap = [
398491
{ key: "Alt-n", run: newFile },
399492
{ key: "Mod-r", run: saveRunFile },
400493
];
401-
const editorExtensions = [
494+
// Extensions that are always present, regardless of file type. The
495+
// per-file language extensions live in `languageCompartment` so they
496+
// can be swapped at runtime (e.g. on Save As to a different
497+
// extension).
498+
const baseEditorExtensions = [
402499
basicSetup,
403500
keymap.of([indentWithTab]),
404501
keymap.of(hotkeyMap),
405502
indentUnit.of(" "),
406-
python(),
407503
editorTheme,
408504
syntaxHighlighting(classHighlighter),
409-
circuitpythonHighlight,
410505
EditorView.updateListener.of(onTextChange)
411506
];
412507

508+
function buildEditorExtensions(path) {
509+
return [
510+
...baseEditorExtensions,
511+
languageCompartment.of(languageExtensionsForPath(path)),
512+
];
513+
}
514+
413515
// Use the editor's function to check if anything has changed
414516
function isDirty() {
415517
if (unchanged == editor.state.doc.length) return false;
416518
return true;
417519
}
418520

419-
function loadEditorContents(content) {
521+
function loadEditorContents(content, path = null) {
420522
editor.setState(EditorState.create({
421523
doc: content,
422-
extensions: editorExtensions
524+
extensions: buildEditorExtensions(path)
423525
}));
526+
// Keep our tracked language path in sync with the fresh state's
527+
// compartment contents so the next setEditorLanguageForPath call
528+
// can correctly skip a no-op reconfigure.
529+
editorLanguagePath = path;
424530
unchanged = editor.state.doc.length;
425531
//console.log("doc length", unchanged);
426532
}
@@ -484,7 +590,9 @@ const MAX_SAVE_RETRIES = 3;
484590

485591
// Save the File Contents and update the UI
486592
async function saveFileContents(path) {
487-
// If this is a different file, we write everything
593+
// If this is a different file, we write everything. The language
594+
// plugin is refreshed by setFilename below (it routes through
595+
// setEditorLanguageForPath), so no extra dispatch is needed here.
488596
if (path !== workflow.currentFilename) {
489597
unchanged = 0;
490598
}
@@ -525,7 +633,7 @@ async function saveFileContents(path) {
525633
// Load the File Contents and Path into the UI
526634
function loadFileContents(path, contents, saved = true) {
527635
setFilename(path);
528-
loadEditorContents(contents);
636+
loadEditorContents(contents, path);
529637
if (saved !== null) {
530638
setSaved(saved);
531639
}
@@ -573,7 +681,7 @@ function disconnectCallback() {
573681
editor = new EditorView({
574682
state: EditorState.create({
575683
doc: "",
576-
extensions: editorExtensions
684+
extensions: buildEditorExtensions(null)
577685
}),
578686
parent: document.querySelector('#editor')
579687
});

package-lock.json

Lines changed: 155 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)