@duet3d/monacotokens 3.7.0-alpha.11 → 3.7.0-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,11 @@
2
2
  * One enumerated value of a parameter (e.g. "1" for G1 H meaning "Stop on endstop")
3
3
  */
4
4
  export interface GcodeParameterValue {
5
- /** Literal value as it appears in source (e.g. "0", "1", "-1") */
6
- value: string;
5
+ /**
6
+ * Literal value as it appears in source (e.g. "0", "1", "-1"). An array lists equivalent literals that mean
7
+ * the same thing, e.g. M669 K accepts both the kinematics name and its legacy number ("coreXY" and "1").
8
+ */
9
+ value: string | string[];
7
10
  /** Human-readable meaning */
8
11
  description: string;
9
12
  }
@@ -31,6 +34,23 @@ export interface GcodeUnprecedentedParameter {
31
34
  /** Description shown in the tooltip when this position is active */
32
35
  description: string;
33
36
  }
37
+ /**
38
+ * Description of a parameter that exists once per machine axis (e.g. the per-axis speed limits of M203). Instead of
39
+ * listing X/Y/Z/U/... explicitly, a code carries a single `axisParameter` that the completion provider expands into
40
+ * one `GcodeParameter` per axis at runtime, so the suggestion list matches the machine that is actually connected.
41
+ */
42
+ export interface GcodeAxisParameter {
43
+ /** Description shown for each generated axis parameter. The placeholder `{axis}` is replaced with the axis letter. */
44
+ description: string;
45
+ /** Optional enumeration of valid values, shared by every generated axis parameter */
46
+ values?: GcodeParameterValue[];
47
+ /**
48
+ * Whether the parameter list follows the live machine configuration. Defaults to `true`: expand to the axes the
49
+ * connected machine reports (falling back to the standard letters when no machine is connected). Set to `false`
50
+ * for axis-defining codes (e.g. M584, M669) that must offer every standard axis letter regardless of configuration.
51
+ */
52
+ dynamic?: boolean;
53
+ }
34
54
  /**
35
55
  * Description of one G/M/T-code (RRF dialect)
36
56
  */
@@ -45,6 +65,8 @@ export interface GcodeInfo {
45
65
  unprecedentedParameter?: GcodeUnprecedentedParameter;
46
66
  /** Parameter letters this code understands */
47
67
  parameters: GcodeParameter[];
68
+ /** Per-axis parameter expanded into one entry per configured axis at completion time (e.g. M203 speed limits) */
69
+ axisParameter?: GcodeAxisParameter;
48
70
  /** If set, this code is deprecated; the string is shown as the reason */
49
71
  deprecated?: string;
50
72
  }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./monaco-menu";
4
4
  export * from "./gcodes";
5
5
  export * from "./expressions";
6
6
  export * from "./objectmodel/machine-context";
7
+ export * from "./objectmodel/axes";
7
8
  export * from "./gcodes/local-variables";
8
9
  export * from "./objectmodel/deprecations";
9
10
  export * from "./objectmodel/enums";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export * from "./monaco-menu";
4
4
  export * from "./gcodes";
5
5
  export * from "./expressions";
6
6
  export * from "./objectmodel/machine-context";
7
+ export * from "./objectmodel/axes";
7
8
  export * from "./gcodes/local-variables";
8
9
  export * from "./objectmodel/deprecations";
9
10
  export * from "./objectmodel/enums";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Supported axis letters, mirroring `Axis.Letters` in DuetAPI (DuetSoftwareFramework). This is the canonical set
3
+ * of identifiers RepRapFirmware accepts for an axis. Kept in sync by hand - update it if DuetAPI gains new letters.
4
+ */
5
+ export declare const ALL_AXIS_LETTERS: readonly string[];
6
+ /**
7
+ * Axis letters offered by completion when the live machine configuration is unavailable (no machine connected) or
8
+ * when an axis parameter is non-dynamic (axis-defining codes such as M584 that may reference axes which do not
9
+ * exist yet). Restricted to the three axes every standard machine has; additional axes (U, V, W, ...) are surfaced
10
+ * dynamically from the connected machine's object model, and any axis can still be typed manually.
11
+ */
12
+ export declare const DEFAULT_AXIS_LETTERS: readonly string[];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Supported axis letters, mirroring `Axis.Letters` in DuetAPI (DuetSoftwareFramework). This is the canonical set
3
+ * of identifiers RepRapFirmware accepts for an axis. Kept in sync by hand - update it if DuetAPI gains new letters.
4
+ */
5
+ export const ALL_AXIS_LETTERS = [
6
+ "X", "Y", "Z",
7
+ "U", "V", "W",
8
+ "A", "B", "C", "D",
9
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
10
+ "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
11
+ ];
12
+ /**
13
+ * Axis letters offered by completion when the live machine configuration is unavailable (no machine connected) or
14
+ * when an axis parameter is non-dynamic (axis-defining codes such as M584 that may reference axes which do not
15
+ * exist yet). Restricted to the three axes every standard machine has; additional axes (U, V, W, ...) are surfaced
16
+ * dynamically from the connected machine's object model, and any axis can still be typed manually.
17
+ */
18
+ export const DEFAULT_AXIS_LETTERS = ["X", "Y", "Z"];
package/dist/providers.js CHANGED
@@ -1,12 +1,133 @@
1
- import { gcodeData, findGcode } from "./gcodes";
1
+ import { gcodeData, findGcode as findGcodeStatic } from "./gcodes";
2
2
  import { expressionData } from "./expressions";
3
3
  import { getMachineContext } from "./objectmodel/machine-context";
4
+ import { ALL_AXIS_LETTERS, DEFAULT_AXIS_LETTERS } from "./objectmodel/axes";
4
5
  import { getLocalVariables } from "./gcodes/local-variables";
5
6
  import { getMemberDeprecation, getPathDeprecation } from "./objectmodel/deprecations";
6
7
  import { getEnumValuesForPath } from "./objectmodel/enums";
7
8
  // Re-export the runtime-context helpers so consumers (Vue DWC, React DuetWebUI, ...) can install a context
8
9
  // without adding a separate import path
9
10
  export { getMachineContext, onMachineContextChange } from "./objectmodel/machine-context";
11
+ /**
12
+ * Expand a code's `axisParameter` into one concrete `GcodeParameter` per axis letter. Dynamic parameters (the
13
+ * default) follow the connected machine's configured, visible axes and fall back to the standard letters when no
14
+ * machine is connected; non-dynamic parameters always offer the standard letters, for axis-defining codes such as
15
+ * M584/M669 that may reference axes which do not exist yet.
16
+ */
17
+ function expandAxisParameter(axisParameter) {
18
+ let letters = DEFAULT_AXIS_LETTERS;
19
+ if (axisParameter.dynamic !== false) {
20
+ const axes = getMachineContext()?.model?.move?.axes;
21
+ const configured = axes
22
+ ? axes.filter(a => !!a && a.visible !== false && typeof a.letter === "string" && a.letter.length === 1).map(a => a.letter)
23
+ : [];
24
+ if (configured.length > 0) {
25
+ letters = configured;
26
+ }
27
+ }
28
+ return letters.map(letter => ({
29
+ letter,
30
+ description: axisParameter.description.replace(/\{axis\}/g, letter),
31
+ values: axisParameter.values
32
+ }));
33
+ }
34
+ /**
35
+ * Resolve a static code entry against the live machine context, expanding any `axisParameter` into concrete
36
+ * per-axis parameters placed ahead of the fixed parameters. Returns the entry unchanged when it has no axis
37
+ * parameter, so every caller can treat the result like a plain `GcodeInfo`.
38
+ */
39
+ function findGcode(code) {
40
+ const info = findGcodeStatic(code);
41
+ if (!info || !info.axisParameter) {
42
+ return info;
43
+ }
44
+ return { ...info, parameters: [...expandAxisParameter(info.axisParameter), ...info.parameters] };
45
+ }
46
+ /**
47
+ * Canonicalise a parameter/axis token to the letter RepRapFirmware actually uses. RRF folds bare command
48
+ * letters to upper case, so `a` addresses axis `A`; a leading apostrophe escapes a lower-case axis, so `'a`
49
+ * addresses axis `a`. The token passed in may carry that leading apostrophe.
50
+ */
51
+ function canonicalAxisLetter(token) {
52
+ return token.startsWith("'") ? token.slice(1).toLowerCase() : token.toUpperCase();
53
+ }
54
+ /**
55
+ * Render a canonical parameter letter the way it must be written in G-code: a lower-case axis needs the leading
56
+ * apostrophe (otherwise RRF would fold it to upper case), every other letter is written as-is.
57
+ */
58
+ function gcodeAxisToken(letter) {
59
+ return /[a-z]/.test(letter) ? "'" + letter : letter;
60
+ }
61
+ /** Build the per-axis parameter for one axis letter from a code's axisParameter, or undefined when the letter is not a supported axis or the code has no axis parameter. */
62
+ function makeAxisParameter(info, letter) {
63
+ if (!info.axisParameter || !ALL_AXIS_LETTERS.includes(letter)) {
64
+ return undefined;
65
+ }
66
+ return {
67
+ letter,
68
+ description: info.axisParameter.description.replace(/\{axis\}/g, letter),
69
+ values: info.axisParameter.values
70
+ };
71
+ }
72
+ /**
73
+ * Resolve a single parameter for hover and signature use from a canonical letter (see {@link canonicalAxisLetter}).
74
+ * Matches a fixed parameter or an axis already expanded for the connected machine, and otherwise builds the
75
+ * parameter on the fly for any supported axis letter - so hovering or typing an axis the machine is not configured
76
+ * with still surfaces its documentation. This is wider than the completion suggestion list and the parameter
77
+ * summary, which only offer the configured axes.
78
+ */
79
+ function lookupParameter(info, letter) {
80
+ const found = info.parameters.find(p => p.letter === letter);
81
+ return found ?? makeAxisParameter(info, letter);
82
+ }
83
+ /**
84
+ * Collect the canonical parameter letters present at the top level of a code's argument segment, in order. Letters
85
+ * inside "..." strings or {...} expressions are skipped because they belong to a value, not to a parameter - e.g.
86
+ * the `g` in `M98 P"check-filament.g"` and any argument passed on to a macro must not be mistaken for parameters.
87
+ * A leading apostrophe is honoured so a lower-case axis (`'a`) stays distinct from the upper-case letter.
88
+ */
89
+ function scanParameterLetters(segment) {
90
+ const letters = [];
91
+ let inString = false;
92
+ let braceDepth = 0;
93
+ for (let i = 0; i < segment.length; i++) {
94
+ const ch = segment[i];
95
+ if (inString) {
96
+ if (ch === "\"") {
97
+ inString = false;
98
+ }
99
+ continue;
100
+ }
101
+ if (ch === "\"") {
102
+ inString = true;
103
+ continue;
104
+ }
105
+ if (ch === "{") {
106
+ braceDepth++;
107
+ continue;
108
+ }
109
+ if (ch === "}") {
110
+ if (braceDepth > 0) {
111
+ braceDepth--;
112
+ }
113
+ continue;
114
+ }
115
+ if (braceDepth > 0) {
116
+ continue;
117
+ }
118
+ if (ch === ";") {
119
+ break;
120
+ }
121
+ if (/[A-Za-z]/.test(ch)) {
122
+ const prev = i > 0 ? segment[i - 1] : "";
123
+ const next = i + 1 < segment.length ? segment[i + 1] : "";
124
+ if (!/[A-Za-z]/.test(prev) && !/[A-Za-z]/.test(next)) {
125
+ letters.push(canonicalAxisLetter(prev === "'" ? prev + ch : ch));
126
+ }
127
+ }
128
+ }
129
+ return letters;
130
+ }
10
131
  /**
11
132
  * Find the enclosing function call (if any) for the cursor position. Walks back from the end of `beforeCursor`
12
133
  * keeping track of paren depth so that `max(a, min(b,|` correctly reports `min` with argIndex 1, not `max`.
@@ -481,7 +602,8 @@ function findParameterAtCursor(line, codeEndColZero, cursorColOne) {
481
602
  if (!/[A-Za-z]/.test(prev) && !/[A-Za-z]/.test(next)) {
482
603
  const valRange = findParameterValueRange(line, i);
483
604
  if (cursorColOne >= valRange.startCol && cursorColOne < valRange.endCol) {
484
- return { letterColZero: i, letter: ch };
605
+ // A leading apostrophe escapes a lower-case axis (`'a`); a bare letter is folded to upper case
606
+ return { letterColZero: i, letter: canonicalAxisLetter(prev === "'" ? prev + ch : ch) };
485
607
  }
486
608
  i = valRange.endCol - 1;
487
609
  continue;
@@ -602,7 +724,8 @@ function buildParameterDoc(info, p) {
602
724
  if (p.values && p.values.length > 0) {
603
725
  md += "\n\n**Values:**";
604
726
  for (const v of p.values) {
605
- md += `\n- \`${v.value}\` - ${v.description}`;
727
+ const literals = (Array.isArray(v.value) ? v.value : [v.value]).map((x) => `\`${x}\``).join(" / ");
728
+ md += `\n- ${literals} - ${v.description}`;
606
729
  }
607
730
  }
608
731
  const deprecationNote = p.deprecated ?? info.deprecated;
@@ -862,21 +985,16 @@ export function registerProvidersFor(monacoInstance, languageId) {
862
985
  // just typed it, Monaco should show nothing (so the widget auto-closes) rather than list
863
986
  // the very letter that was just typed as the only match
864
987
  const fullTail = lineContent.substring(code.startColumn - 1 + code.code.length);
865
- const used = new Set();
866
- const re = /(?<![A-Za-z])[A-Za-z](?![A-Za-z])/g;
867
- let m;
868
- while ((m = re.exec(fullTail)) !== null) {
869
- used.add(m[0].toUpperCase());
870
- }
988
+ const used = new Set(scanParameterLetters(fullTail));
871
989
  const triggerHints = { id: "editor.action.triggerParameterHints", title: "Trigger Parameter Hints" };
872
990
  const suggestions = info.parameters
873
- .filter(p => !used.has(p.letter.toUpperCase()))
991
+ .filter(p => !used.has(p.letter))
874
992
  .map(p => ({
875
- label: { label: p.letter, description: p.description },
993
+ label: { label: gcodeAxisToken(p.letter), description: p.description },
876
994
  kind: monacoInstance.languages.CompletionItemKind.Property,
877
995
  detail: p.description,
878
996
  documentation: md(buildParameterDoc(info, p)),
879
- insertText: p.letter,
997
+ insertText: gcodeAxisToken(p.letter),
880
998
  range,
881
999
  // Re-open the signature help tooltip after accepting a parameter letter (Monaco otherwise
882
1000
  // closes parameter hints when any completion item is accepted without a command)
@@ -1003,7 +1121,7 @@ export function registerProvidersFor(monacoInstance, languageId) {
1003
1121
  }
1004
1122
  for (const p of info.parameters) {
1005
1123
  const start = label.length + 1;
1006
- label += " " + p.letter;
1124
+ label += " " + gcodeAxisToken(p.letter);
1007
1125
  parameters.push({
1008
1126
  label: [start, label.length],
1009
1127
  documentation: md(buildParameterDoc(info, p))
@@ -1013,18 +1131,31 @@ export function registerProvidersFor(monacoInstance, languageId) {
1013
1131
  // for codes with a unprecedentedParameter, sit on slot 0 while the user is typing that value (no parameter letter typed yet)
1014
1132
  const tail = tailForDismissal;
1015
1133
  let activeParameter = -1;
1016
- const seen = tail.match(/(?<![A-Za-z])[A-Za-z](?![A-Za-z])/g);
1134
+ const seen = scanParameterLetters(tail);
1017
1135
  const unprecedentedOffset = info.unprecedentedParameter ? 1 : 0;
1018
- if (seen && seen.length > 0) {
1019
- const last = seen[seen.length - 1].toUpperCase();
1020
- const idx = info.parameters.findIndex(p => p.letter.toUpperCase() === last);
1136
+ if (seen.length > 0) {
1137
+ const last = seen[seen.length - 1];
1138
+ const idx = info.parameters.findIndex(p => p.letter === last);
1021
1139
  if (idx >= 0) {
1022
1140
  activeParameter = idx + unprecedentedOffset;
1023
1141
  }
1024
1142
  else {
1025
- // User typed a letter that isn't a documented parameter for this code - hide the popup
1026
- // rather than falling back to the generic summary view, which would be misleading
1027
- return null;
1143
+ // Letter isn't one of the listed parameters. If it is a supported axis on an axis code (e.g. a
1144
+ // U axis the connected machine isn't configured with) append it so its documentation still shows;
1145
+ // otherwise hide the popup rather than falling back to the misleading generic summary view
1146
+ const axisParam = makeAxisParameter(info, last);
1147
+ if (axisParam) {
1148
+ const start = label.length + 1;
1149
+ label += " " + gcodeAxisToken(axisParam.letter);
1150
+ parameters.push({
1151
+ label: [start, label.length],
1152
+ documentation: md(buildParameterDoc(info, axisParam))
1153
+ });
1154
+ activeParameter = parameters.length - 1;
1155
+ }
1156
+ else {
1157
+ return null;
1158
+ }
1028
1159
  }
1029
1160
  }
1030
1161
  else if (info.unprecedentedParameter) {
@@ -1096,7 +1227,7 @@ export function registerProvidersFor(monacoInstance, languageId) {
1096
1227
  const info = findGcode(enclosingCode.code);
1097
1228
  const paramAtCursor = info ? findParameterAtCursor(lineContent, enclosingCode.startColumn + enclosingCode.code.length - 1, position.column) : null;
1098
1229
  if (info && paramAtCursor) {
1099
- const param = info.parameters.find(p => p.letter.toUpperCase() === paramAtCursor.letter.toUpperCase());
1230
+ const param = lookupParameter(info, paramAtCursor.letter);
1100
1231
  if (param) {
1101
1232
  const valueRange = findParameterValueRange(lineContent, paramAtCursor.letterColZero);
1102
1233
  return {
@@ -1147,7 +1278,7 @@ export function registerProvidersFor(monacoInstance, languageId) {
1147
1278
  // inside `1:2:3` of `E1:2:3`). This covers hovers that don't land on the letter itself
1148
1279
  const paramAtCursor = findParameterAtCursor(lineContent, enclosing.startColumn + enclosing.code.length - 1, position.column);
1149
1280
  if (info && paramAtCursor) {
1150
- const param = info.parameters.find(p => p.letter.toUpperCase() === paramAtCursor.letter.toUpperCase());
1281
+ const param = lookupParameter(info, paramAtCursor.letter);
1151
1282
  if (param) {
1152
1283
  const valueRange = findParameterValueRange(lineContent, paramAtCursor.letterColZero);
1153
1284
  return {
@@ -1159,7 +1290,9 @@ export function registerProvidersFor(monacoInstance, languageId) {
1159
1290
  // Fallback: hovered word starts with a parameter letter (e.g. bare `X` in `M84 X Y Z`, where
1160
1291
  // the letter has no value after it so findParameterAtCursor may stop before reaching it)
1161
1292
  if (info && /^[A-Za-z]/.test(word.word)) {
1162
- const param = info.parameters.find(p => p.letter.toUpperCase() === firstLetter.toUpperCase());
1293
+ // A leading apostrophe just before the word marks a lower-case axis (`'a`)
1294
+ const escaped = word.startColumn >= 2 && lineContent[word.startColumn - 2] === "'";
1295
+ const param = lookupParameter(info, canonicalAxisLetter(escaped ? "'" + firstLetter : firstLetter));
1163
1296
  if (param) {
1164
1297
  const valueRange = findParameterValueRange(lineContent, word.startColumn - 1);
1165
1298
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duet3d/monacotokens",
3
- "version": "3.7.0-alpha.11",
3
+ "version": "3.7.0-alpha.13",
4
4
  "description": "TypeScript library that holds syntax highlighting files for the Monaco editor",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",