Skip to content

Commit b3bb3dc

Browse files
author
lakshyabagani-prog
committed
Replace stapler-adjunct-codemirror (CM2) with modern CodeMirror 6
Remove the legacy stapler-adjunct-codemirror dependency (CodeMirror 2) and replace it with CodeMirror 6 from NPM, resolving issue #18689. - Add CM6 as a separate webpack entry point to avoid bloating vendors.js - Bundle only @codemirror/lang-java for Groovy/Java syntax highlighting - Provide backward-compatible codemirrorObject shim on textareas - Theme uses Jenkins CSS variables for consistent look and feel - Support Cmd/Ctrl+Enter form submission via CM6 keymap - Remove 99 lines of CM2 init code from hudson-behavior.js - Remove 193 lines of CM2 styles from _codemirror.scss - Delete orphaned mask-codemirror.svg - Update FormFieldValidatorTest selectors for CM6 DOM structure - Use <st:once> to deduplicate script loading on multi-textarea pages
1 parent 057aa6a commit b3bb3dc

File tree

17 files changed

+590
-366
lines changed

17 files changed

+590
-366
lines changed

bom/pom.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,6 @@ THE SOFTWARE.
312312
<artifactId>stapler</artifactId>
313313
<version>${stapler.version}</version>
314314
</dependency>
315-
<dependency>
316-
<groupId>org.kohsuke.stapler</groupId>
317-
<artifactId>stapler-adjunct-codemirror</artifactId>
318-
<version>1.3</version>
319-
</dependency>
320315
<dependency>
321316
<groupId>org.kohsuke.stapler</groupId>
322317
<artifactId>stapler-groovy</artifactId>

core/pom.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,6 @@ THE SOFTWARE.
352352
<groupId>org.kohsuke.stapler</groupId>
353353
<artifactId>stapler</artifactId>
354354
</dependency>
355-
<dependency>
356-
<groupId>org.kohsuke.stapler</groupId>
357-
<artifactId>stapler-adjunct-codemirror</artifactId>
358-
</dependency>
359355
<dependency>
360356
<groupId>org.kohsuke.stapler</groupId>
361357
<artifactId>stapler-groovy</artifactId>

core/src/main/resources/lib/form/textarea.jelly

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ THE SOFTWARE.
8484
<st:adjunct includes="lib.form.textarea.textarea"/>
8585
</j:if>
8686
<j:if test="${attrs['codemirror-mode']!=null}">
87-
<st:adjunct includes="
88-
org.kohsuke.stapler.codemirror.mode.${attrs['codemirror-mode']}.${attrs['codemirror-mode']},
89-
org.kohsuke.stapler.codemirror.theme.default"/>
87+
<st:once>
88+
<script src="${resURL}/jsbundles/codemirror.js" type="text/javascript" defer="true" />
89+
</st:once>
9090
</j:if>
9191
<j:set var="name" value="${attrs.name ?: '_.'+attrs.field}"/>
9292
<f:possibleReadOnlyField>

core/src/main/resources/lib/form/textarea/textarea.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
td.setting-main .CodeMirror {
1+
td.setting-main .cm-editor {
22
display: table;
33
table-layout: fixed;
44
width: 100%;

core/src/main/resources/lib/form/textarea/textarea.js

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,3 @@
1-
Behaviour.specify("TEXTAREA.codemirror", "textarea", 0, function (e) {
2-
var config = e.getAttribute("codemirror-config");
3-
if (!config) {
4-
config = "";
5-
}
6-
try {
7-
config = JSON.parse("{" + config + "}");
8-
} catch (ex) {
9-
/*
10-
* Attempt to parse fairly common legacy format whose exact content is:
11-
* mode:'<MIME>'
12-
*/
13-
let match = config.match("^mode: ?'([^']+)'$");
14-
if (match) {
15-
console.log(
16-
"Parsing simple legacy codemirror-config value using fallback: " +
17-
config,
18-
);
19-
config = { mode: match[1] };
20-
} else {
21-
console.log(
22-
"Failed to parse codemirror-config '{" + config + "}' as JSON",
23-
ex,
24-
);
25-
config = {};
26-
}
27-
}
28-
if (!config.onBlur) {
29-
config.onBlur = function (editor) {
30-
editor.save();
31-
editor.getTextArea().dispatchEvent(new Event("change"));
32-
};
33-
}
34-
var codemirror = CodeMirror.fromTextArea(e, config);
35-
e.codemirrorObject = codemirror;
36-
if (typeof codemirror.getScrollerElement !== "function") {
37-
// Maybe older versions of CodeMirror do not provide getScrollerElement method.
38-
codemirror.getScrollerElement = function () {
39-
return codemirror.getWrapperElement().querySelector(".CodeMirror-scroll");
40-
};
41-
}
42-
var lineCount = codemirror.lineCount();
43-
var lineHeight = codemirror.defaultTextHeight();
44-
45-
var scroller = codemirror.getScrollerElement();
46-
scroller.setAttribute("style", "border:none;");
47-
scroller.style.height = Math.max(lineHeight * lineCount + 30, 130) + "px";
48-
49-
// the form needs to be populated before the "Apply" button
50-
if (e.closest("form")) {
51-
// Protect against undefined element
52-
e.closest("form").addEventListener("jenkins:apply", function () {
53-
e.value = codemirror.getValue();
54-
});
55-
}
56-
});
57-
581
Behaviour.specify(
592
"DIV.textarea-preview-container",
603
"textarea",

core/src/main/resources/lib/hudson/scriptConsole.jelly

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,13 @@ THE SOFTWARE.
5656
${%description2}
5757
</p>
5858

59+
<script src="${resURL}/jsbundles/codemirror.js" type="text/javascript" defer="true" />
5960
<form action="script" method="post">
6061
<textarea id="script" name="script" class="script">${request2.getParameter('script')}</textarea>
6162
<div align="right">
6263
<f:submit value="${%Run}"/>
6364
</div>
6465
</form>
65-
<st:adjunct includes="org.kohsuke.stapler.codemirror.mode.groovy.groovy"/>
66-
<st:adjunct includes="org.kohsuke.stapler.codemirror.theme.default"/>
6766
<j:if test="${output!=null}">
6867
<h2>
6968
${%Result}

eslint.config.cjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ module.exports = [
3232
Behaviour: "readonly",
3333
breadcrumbs: "readonly",
3434
buildFormTree: "readonly",
35-
CodeMirror: "readonly",
3635
ComboBox: "readonly",
3736
COMBOBOX_VERSION: "writeable",
3837
crumb: "readonly",

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@
5353
"webpack-remove-empty-scripts": "1.1.1"
5454
},
5555
"dependencies": {
56+
"@codemirror/lang-java": "6.0.2",
57+
"@codemirror/language": "6.10.8",
58+
"@codemirror/state": "6.5.1",
59+
"@codemirror/view": "6.36.2",
60+
"@lezer/common": "1.2.3",
61+
"@lezer/highlight": "1.2.1",
62+
"codemirror": "6.0.1",
5663
"handlebars": "4.7.8",
5764
"hotkeys-js": "3.12.2",
5865
"jquery": "4.0.0",

src/main/js/codemirror.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import CodeMirrorEditor from "@/components/codemirror";
2+
3+
CodeMirrorEditor.init();
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { EditorView, basicSetup } from "codemirror";
2+
import { EditorState } from "@codemirror/state";
3+
import { keymap } from "@codemirror/view";
4+
import { codeEditorTheme } from "@/components/codemirror/theme";
5+
import behaviorShim from "@/util/behavior-shim";
6+
7+
// Java language support — also used for Groovy (syntactically very similar, no CM6 Groovy parser exists)
8+
import { java } from "@codemirror/lang-java";
9+
10+
// Map CM2 mode names/MIME types to CM6 language support.
11+
// Only Java/Groovy is bundled in core. Plugins needing other languages
12+
// can contribute additional language support.
13+
const LANGUAGES = {
14+
groovy: java,
15+
"text/x-groovy": java,
16+
java: java,
17+
"text/x-java": java,
18+
clike: java,
19+
"text/x-csrc": java,
20+
"text/x-c++src": java,
21+
"text/x-csharp": java,
22+
};
23+
24+
function getLanguageSupport(mode) {
25+
if (!mode) {
26+
return null;
27+
}
28+
const factory = LANGUAGES[mode] ?? LANGUAGES[mode.toLowerCase()];
29+
if (typeof factory === "function") {
30+
return factory();
31+
}
32+
// Unknown modes get plain text editing (no syntax highlighting)
33+
return null;
34+
}
35+
36+
function createEditor(textarea, options) {
37+
const mode = options.mode;
38+
const lineNumbers =
39+
options.lineNumbers !== undefined ? options.lineNumbers : false;
40+
const readOnly = textarea.hasAttribute("readonly");
41+
42+
const extensions = [basicSetup, codeEditorTheme];
43+
44+
const languageSupport = getLanguageSupport(mode);
45+
if (languageSupport) {
46+
extensions.push(languageSupport);
47+
}
48+
49+
if (readOnly) {
50+
extensions.push(EditorState.readOnly.of(true));
51+
}
52+
53+
// Sync changes back to the textarea on every update
54+
extensions.push(
55+
EditorView.updateListener.of((update) => {
56+
if (update.docChanged) {
57+
textarea.value = update.state.doc.toString();
58+
textarea.dispatchEvent(new Event("change"));
59+
}
60+
}),
61+
);
62+
63+
if (!lineNumbers) {
64+
extensions.push(EditorView.lineWrapping);
65+
}
66+
67+
const view = new EditorView({
68+
state: EditorState.create({
69+
doc: textarea.value,
70+
extensions: extensions,
71+
}),
72+
});
73+
74+
// Handle Cmd/Ctrl+Enter to submit the form
75+
if (textarea.form) {
76+
const submitKeymap = keymap.of([
77+
{
78+
key: "Mod-Enter",
79+
run: () => {
80+
textarea.value = view.state.doc.toString();
81+
textarea.form.submit();
82+
return true;
83+
},
84+
},
85+
]);
86+
view.dispatch({
87+
effects: EditorState.appendConfig.of(submitKeymap),
88+
});
89+
}
90+
91+
textarea.parentNode.insertBefore(view.dom, textarea);
92+
textarea.style.display = "none";
93+
94+
// Store reference on textarea for backward compatibility
95+
textarea.codemirrorObject = {
96+
getValue: () => view.state.doc.toString(),
97+
setValue: (text) => {
98+
view.dispatch({
99+
changes: { from: 0, to: view.state.doc.length, insert: text },
100+
});
101+
},
102+
setLine: (line, text) => {
103+
const lineObj = view.state.doc.line(line + 1);
104+
view.dispatch({
105+
changes: { from: lineObj.from, to: lineObj.to, insert: text },
106+
});
107+
},
108+
save: () => {
109+
textarea.value = view.state.doc.toString();
110+
},
111+
getView: () => view,
112+
};
113+
114+
// Sync value before form submission
115+
if (textarea.form) {
116+
textarea.form.addEventListener("submit", () => {
117+
textarea.value = view.state.doc.toString();
118+
});
119+
textarea.form.addEventListener("jenkins:apply", () => {
120+
textarea.value = view.state.doc.toString();
121+
});
122+
}
123+
124+
return view;
125+
}
126+
127+
function init() {
128+
// Handle textareas with codemirror-mode attribute (from f:textarea)
129+
behaviorShim.specify(
130+
"TEXTAREA.codemirror",
131+
"codemirror-textarea",
132+
0,
133+
(textarea) => {
134+
const mode = textarea.getAttribute("codemirror-mode") || "text/x-groovy";
135+
let config = {};
136+
const configAttr = textarea.getAttribute("codemirror-config");
137+
if (configAttr) {
138+
try {
139+
config = JSON.parse("{" + configAttr + "}");
140+
} catch {
141+
const match = configAttr.match("^mode: ?'([^']+)'$");
142+
if (match) {
143+
config = { mode: match[1] };
144+
}
145+
}
146+
}
147+
createEditor(textarea, {
148+
mode: config.mode || mode,
149+
lineNumbers: false,
150+
});
151+
},
152+
);
153+
154+
// Handle Script Console textareas
155+
behaviorShim.specify(
156+
"TEXTAREA.script",
157+
"codemirror-script-console",
158+
0,
159+
(textarea) => {
160+
const mode = textarea.getAttribute("script-mode") || "text/x-groovy";
161+
createEditor(textarea, {
162+
mode: mode,
163+
lineNumbers: true,
164+
});
165+
},
166+
);
167+
}
168+
169+
export default { init };

0 commit comments

Comments
 (0)