From a67a86f185cf63cbd183f354bf93c02359468492 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Fri, 21 Aug 2026 19:05:21 +0100 Subject: [PATCH] Add G-code Language Support for CodeMirror - Introduced G-code command descriptions and a lookup function for tooltips in the new commands.js file. - Implemented G-code language support in CodeMirror with syntax highlighting and tooltip functionality in the new index.js file. - Updated codeBlockEditorUtils.js to include G-code language support, allowing for enhanced editing capabilities for G-code files. --- src/codemirror/gcodeLang/commands.js | 102 ++++++++++++++++ src/codemirror/gcodeLang/index.js | 109 ++++++++++++++++++ .../Dashboard/common/codeBlockEditorUtils.js | 5 + 3 files changed, 216 insertions(+) create mode 100644 src/codemirror/gcodeLang/commands.js create mode 100644 src/codemirror/gcodeLang/index.js diff --git a/src/codemirror/gcodeLang/commands.js b/src/codemirror/gcodeLang/commands.js new file mode 100644 index 00000000..2ccbca58 --- /dev/null +++ b/src/codemirror/gcodeLang/commands.js @@ -0,0 +1,102 @@ +/** + * G-code command descriptions for hover tooltips. + * Adapted from gcode-lang-codemirror (MIT): + * https://github.com/oneislandearth/gcode-lang-codemirror + */ +export const GCodeCommands = { + G0: 'Rapid movement', + G1: 'Linear movement', + G2: 'Clockwise arc movement', + G3: 'Counter-clockwise arc movement', + G4: 'Dwell (wait P milliseconds or S seconds)', + G10: 'Retract (firmware retraction)', + G11: 'Recover (firmware retraction)', + G20: 'Set units to inches', + G21: 'Set units to millimeters', + G28: 'Home axes', + G29: 'Automatic bed leveling', + G80: 'Mesh bed leveling (Prusa) / cancel canned cycle', + G90: 'Use absolute positioning', + G91: 'Use relative positioning', + G92: 'Set current position', + M0: 'Stop after the buffer is empty', + M17: 'Enable stepper motors', + M18: 'Disable stepper motors', + M20: 'List SD card files', + M21: 'Initialize (mount) SD card', + M22: 'Release (unmount) SD card', + M23: 'Select SD file for printing', + M24: 'Start / resume SD print', + M25: 'Pause SD print', + M26: 'Set SD position in bytes', + M27: 'Report SD print status', + M28: 'Start writing to SD card', + M29: 'Stop writing to SD card', + M40: 'Eject part', + M42: 'Set pin state / stop if out of material', + M73: 'Set print progress', + M80: 'Turn ATX power on', + M81: 'Turn ATX power off', + M82: 'Extruder absolute mode', + M83: 'Extruder relative mode', + M84: 'Disable idle hold / steppers', + M92: 'Set steps per unit', + M104: 'Set extruder temperature', + M105: 'Get temperatures', + M106: 'Set fan speed', + M107: 'Turn fan off', + M109: 'Set extruder temperature and wait', + M110: 'Set current line number', + M112: 'Emergency stop', + M114: 'Get current position', + M115: 'Get firmware version and capabilities', + M117: 'Set LCD / status message', + M119: 'Get endstop status', + M140: 'Set bed temperature', + M141: 'Set chamber temperature', + M190: 'Set bed temperature and wait', + M191: 'Set chamber temperature and wait', + M220: 'Set feedrate percentage', + M221: 'Set flow percentage', + M300: 'Beep (S Hz for P ms)', + M600: 'Filament change', + M601: 'Pause print (Prusa)', + M862: 'G-code compatibility check (Prusa)', + M900: 'Linear / pressure advance (K)', + T0: 'Select tool / extruder 0', + T1: 'Select tool / extruder 1', + X: 'X axis', + Y: 'Y axis', + Z: 'Z axis', + E: 'Extruder axis', + A: 'A axis', + B: 'B axis', + C: 'C axis', + F: 'Feedrate', + S: 'Speed, temperature, or value', + P: 'Parameter / time', + I: 'Arc X offset / parameter', + J: 'Arc Y offset / parameter', + K: 'Arc Z offset / linear advance', + R: 'Arc radius / RPM' +} + +export function lookupGCodeCommand(word) { + const upper = String(word || '').toUpperCase() + const commandMatch = upper.match(/^([GMT])0*(\d+)(\.\d+)?/) + if (commandMatch) { + const base = commandMatch[1] + String(parseInt(commandMatch[2], 10)) + const canonical = base + (commandMatch[3] || '') + return ( + GCodeCommands[canonical] || + GCodeCommands[base] || + GCodeCommands[upper] || + null + ) + } + const axis = upper.charAt(0) + if (axis && GCodeCommands[axis]) { + return GCodeCommands[axis] + } + return null +} diff --git a/src/codemirror/gcodeLang/index.js b/src/codemirror/gcodeLang/index.js new file mode 100644 index 00000000..09258d3b --- /dev/null +++ b/src/codemirror/gcodeLang/index.js @@ -0,0 +1,109 @@ +import { + StreamLanguage, + LanguageSupport +} from '@codemirror/language' +import { EditorView, hoverTooltip } from '@codemirror/view' +import { lookupGCodeCommand } from './commands.js' + +/** + * G-code language support for CodeMirror 6. + * Highlighting and command tooltips adapted from gcode-lang-codemirror: + * https://github.com/oneislandearth/gcode-lang-codemirror + * + * Implemented as a StreamLanguage so typical 3D-printer G-code (no `%` + * wrappers, E axis, slicer `{placeholders}` / `[placeholders]`) highlights + * without the CodeMirror 0.19 dependencies of the original package. + */ +const wordChar = /[A-Za-z0-9.]/ + +function gcodeToken(stream) { + if (stream.eatSpace()) return null + + if (stream.eat(';')) { + stream.skipToEnd() + return 'comment' + } + + if (stream.eat('(')) { + if (!stream.skipTo(')')) stream.skipToEnd() + else stream.eat(')') + return 'comment' + } + + if (stream.eat('{')) { + if (!stream.skipTo('}')) stream.skipToEnd() + else stream.eat('}') + return 'processingInstruction' + } + + if (stream.eat('[')) { + if (!stream.skipTo(']')) stream.skipToEnd() + else stream.eat(']') + return 'processingInstruction' + } + + if (stream.match(/^[GgMmTt]\d+(\.\d+)?/)) return 'keyword' + if (stream.match(/^[Nn]\d+/)) return 'meta' + if (stream.match(/^\*\d+/)) return 'meta' + if (stream.eat('%')) return 'meta' + + if (stream.match(/^[A-Za-z]/)) return 'strong' + if (stream.match(/^[+-]?(\d+\.?\d*|\.\d+)/)) return 'number' + + stream.next() + return null +} + +export const gcodeLanguage = StreamLanguage.define({ + name: 'gcode', + token: gcodeToken, + languageData: { + commentTokens: { line: ';', block: { open: '(', close: ')' } } + } +}) + +export const gcodeCommandTooltips = hoverTooltip((view, pos, side) => { + const line = view.state.doc.lineAt(pos) + let start = pos + let end = pos + const charAt = (offset) => line.text.charAt(offset - line.from) + + while (start > line.from && wordChar.test(charAt(start - 1))) start-- + while (end < line.to && wordChar.test(charAt(end))) end++ + if ((start === pos && side < 0) || (end === pos && side > 0)) { + return null + } + + const word = line.text.slice(start - line.from, end - line.from) + const description = lookupGCodeCommand(word) + if (!description) return null + + return { + pos: start, + end, + above: true, + create() { + const dom = document.createElement('div') + dom.className = 'cm-tooltip-gcode' + dom.textContent = description + return { dom } + } + } +}) + +const gcodeTooltipTheme = EditorView.theme({ + '.cm-tooltip-gcode': { + padding: '4px 8px', + fontSize: '12px' + } +}) + +export function gcodeLang() { + return new LanguageSupport(gcodeLanguage, [ + gcodeCommandTooltips, + gcodeTooltipTheme + ]) +} + +export { gcodeLang as GCodeLanguage } +export { GCodeCommands, lookupGCodeCommand } from './commands.js' diff --git a/src/components/Dashboard/common/codeBlockEditorUtils.js b/src/components/Dashboard/common/codeBlockEditorUtils.js index 0b53bc43..639c649e 100644 --- a/src/components/Dashboard/common/codeBlockEditorUtils.js +++ b/src/components/Dashboard/common/codeBlockEditorUtils.js @@ -13,6 +13,7 @@ import { php } from '@codemirror/lang-php' import { yaml } from '@codemirror/lang-yaml' import { xml } from '@codemirror/lang-xml' import { fcTemplateLang } from '../../../codemirror/fcTemplateLang' +import { gcodeLang } from '../../../codemirror/gcodeLang' export function toEditorCode(code, language = 'javascript') { if (code == null) return '' @@ -67,6 +68,10 @@ export function getCodeLanguageExtension( case 'yaml': case 'yml': return yaml() + case 'gcode': + case 'g-code': + case 'g': + return gcodeLang() default: return javascriptLang({ autoCompleteObject }) }