Skip to content
Closed
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
5 changes: 0 additions & 5 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,6 @@ THE SOFTWARE.
<artifactId>stapler</artifactId>
<version>${stapler.version}</version>
</dependency>
<dependency>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>stapler-adjunct-codemirror</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>stapler-groovy</artifactId>
Expand Down
4 changes: 0 additions & 4 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,6 @@ THE SOFTWARE.
<groupId>org.kohsuke.stapler</groupId>
<artifactId>stapler</artifactId>
</dependency>
<dependency>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>stapler-adjunct-codemirror</artifactId>
</dependency>
<dependency>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>stapler-groovy</artifactId>
Expand Down
6 changes: 3 additions & 3 deletions core/src/main/resources/lib/form/textarea.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ THE SOFTWARE.
<st:adjunct includes="lib.form.textarea.textarea"/>
</j:if>
<j:if test="${attrs['codemirror-mode']!=null}">
<st:adjunct includes="
org.kohsuke.stapler.codemirror.mode.${attrs['codemirror-mode']}.${attrs['codemirror-mode']},
org.kohsuke.stapler.codemirror.theme.default"/>
<st:once>
<script src="${resURL}/jsbundles/codemirror.js" type="text/javascript" defer="true" />
</st:once>
</j:if>
<j:set var="name" value="${attrs.name ?: '_.'+attrs.field}"/>
<f:possibleReadOnlyField>
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/resources/lib/form/textarea/textarea.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
td.setting-main .CodeMirror {
td.setting-main .cm-editor {
display: table;
table-layout: fixed;
width: 100%;
Expand Down
57 changes: 0 additions & 57 deletions core/src/main/resources/lib/form/textarea/textarea.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,3 @@
Behaviour.specify("TEXTAREA.codemirror", "textarea", 0, function (e) {
var config = e.getAttribute("codemirror-config");
if (!config) {
config = "";
}
try {
config = JSON.parse("{" + config + "}");
} catch (ex) {
/*
* Attempt to parse fairly common legacy format whose exact content is:
* mode:'<MIME>'
*/
let match = config.match("^mode: ?'([^']+)'$");
if (match) {
console.log(
"Parsing simple legacy codemirror-config value using fallback: " +
config,
);
config = { mode: match[1] };
} else {
console.log(
"Failed to parse codemirror-config '{" + config + "}' as JSON",
ex,
);
config = {};
}
}
if (!config.onBlur) {
config.onBlur = function (editor) {
editor.save();
editor.getTextArea().dispatchEvent(new Event("change"));
};
}
var codemirror = CodeMirror.fromTextArea(e, config);
e.codemirrorObject = codemirror;
if (typeof codemirror.getScrollerElement !== "function") {
// Maybe older versions of CodeMirror do not provide getScrollerElement method.
codemirror.getScrollerElement = function () {
return codemirror.getWrapperElement().querySelector(".CodeMirror-scroll");
};
}
var lineCount = codemirror.lineCount();
var lineHeight = codemirror.defaultTextHeight();

var scroller = codemirror.getScrollerElement();
scroller.setAttribute("style", "border:none;");
scroller.style.height = Math.max(lineHeight * lineCount + 30, 130) + "px";

// the form needs to be populated before the "Apply" button
if (e.closest("form")) {
// Protect against undefined element
e.closest("form").addEventListener("jenkins:apply", function () {
e.value = codemirror.getValue();
});
}
});

Behaviour.specify(
"DIV.textarea-preview-container",
"textarea",
Expand Down
3 changes: 1 addition & 2 deletions core/src/main/resources/lib/hudson/scriptConsole.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,13 @@ THE SOFTWARE.
${%description2}
</p>

<script src="${resURL}/jsbundles/codemirror.js" type="text/javascript" defer="true" />
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scriptConsole.jelly loads codemirror.js directly without <st:once>, while textarea.jelly uses <st:once> for deduplication. For consistency and to guard against future edge cases where the script console component might be included multiple times, wrapping the script tag with <st:once> would be appropriate. Although the script console is typically only present once per page, using <st:once> matches the pattern established in textarea.jelly.

Suggested change
<script src="${resURL}/jsbundles/codemirror.js" type="text/javascript" defer="true" />
<st:once>
<script src="${resURL}/jsbundles/codemirror.js" type="text/javascript" defer="true" />
</st:once>

Copilot uses AI. Check for mistakes.
<form action="script" method="post">
<textarea id="script" name="script" class="script">${request2.getParameter('script')}</textarea>
<div align="right">
<f:submit value="${%Run}"/>
</div>
</form>
<st:adjunct includes="org.kohsuke.stapler.codemirror.mode.groovy.groovy"/>
<st:adjunct includes="org.kohsuke.stapler.codemirror.theme.default"/>
<j:if test="${output!=null}">
<h2>
${%Result}
Expand Down
1 change: 0 additions & 1 deletion eslint.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ module.exports = [
Behaviour: "readonly",
breadcrumbs: "readonly",
buildFormTree: "readonly",
CodeMirror: "readonly",
ComboBox: "readonly",
COMBOBOX_VERSION: "writeable",
crumb: "readonly",
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@
"webpack-remove-empty-scripts": "1.1.1"
},
"dependencies": {
"@codemirror/lang-java": "6.0.2",
"@codemirror/language": "6.10.8",
"@codemirror/state": "6.5.1",
"@codemirror/view": "6.36.2",
"@lezer/common": "1.2.3",
"@lezer/highlight": "1.2.1",
"codemirror": "6.0.1",
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The codemirror package is pinned to 6.0.1 (the very first CM6 release from 2022) in package.json, while the individual CM6 sub-packages (@codemirror/view, @codemirror/state, etc.) are pinned to more recent versions. Because codemirror@6.0.1 depends on these sub-packages via ^6.0.0 ranges, and the project also has direct exact-version pins on the sub-packages, there may be two different resolved versions of the same packages in the yarn lockfile (e.g., @codemirror/view@6.36.2 for direct imports and @codemirror/view@6.39.15 for codemirror's transitive dependency). This can cause subtle bugs because CM6 packages use shared state via Facets, and having two different versions of the same package in the bundle means these instances won't interoperate correctly. Consider upgrading codemirror to a recent release (e.g., 6.0.1 → latest) that aligns with the sub-package versions, or relying solely on the sub-packages without the codemirror meta-package.

Suggested change
"codemirror": "6.0.1",

Copilot uses AI. Check for mistakes.
"handlebars": "4.7.8",
"hotkeys-js": "3.12.2",
"jquery": "4.0.0",
Expand Down
3 changes: 3 additions & 0 deletions src/main/js/codemirror.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import CodeMirrorEditor from "@/components/codemirror";

CodeMirrorEditor.init();
169 changes: 169 additions & 0 deletions src/main/js/components/codemirror/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { keymap } from "@codemirror/view";
import { codeEditorTheme } from "@/components/codemirror/theme";
import behaviorShim from "@/util/behavior-shim";

// Java language support — also used for Groovy (syntactically very similar, no CM6 Groovy parser exists)
import { java } from "@codemirror/lang-java";

// Map CM2 mode names/MIME types to CM6 language support.
// Only Java/Groovy is bundled in core. Plugins needing other languages
// can contribute additional language support.
const LANGUAGES = {
groovy: java,
"text/x-groovy": java,
java: java,
"text/x-java": java,
clike: java,
"text/x-csrc": java,
"text/x-c++src": java,
"text/x-csharp": java,
};

function getLanguageSupport(mode) {
if (!mode) {
return null;
}
const factory = LANGUAGES[mode] ?? LANGUAGES[mode.toLowerCase()];
if (typeof factory === "function") {
return factory();
}
// Unknown modes get plain text editing (no syntax highlighting)
return null;
}

function createEditor(textarea, options) {
const mode = options.mode;
const lineNumbers =
options.lineNumbers !== undefined ? options.lineNumbers : false;
const readOnly = textarea.hasAttribute("readonly");

const extensions = [basicSetup, codeEditorTheme];

const languageSupport = getLanguageSupport(mode);
if (languageSupport) {
extensions.push(languageSupport);
}

if (readOnly) {
extensions.push(EditorState.readOnly.of(true));
}

// Sync changes back to the textarea on every update
extensions.push(
EditorView.updateListener.of((update) => {
if (update.docChanged) {
textarea.value = update.state.doc.toString();
textarea.dispatchEvent(new Event("change"));
}
}),
);

if (!lineNumbers) {
extensions.push(EditorView.lineWrapping);
Comment on lines +42 to +64
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lineNumbers option does not correctly disable line numbers. When lineNumbers: false, the code adds EditorView.lineWrapping but does not remove the line numbers extension, which is already included by basicSetup. As a result, all editors (including form textareas with lineNumbers: false) will display line numbers. To conditionally include line numbers, you should use minimalSetup and add individual extensions, or use a Compartment to conditionally include/exclude the lineNumbers() extension.

If line wrapping is always desired for TEXTAREA.codemirror (form fields), and line numbers are only desired for TEXTAREA.script (script console), the basicSetup approach should be replaced with a setup that doesn't unconditionally include lineNumbers().

Copilot uses AI. Check for mistakes.
}

const view = new EditorView({
state: EditorState.create({
doc: textarea.value,
extensions: extensions,
}),
});

// Handle Cmd/Ctrl+Enter to submit the form
if (textarea.form) {
const submitKeymap = keymap.of([
{
key: "Mod-Enter",
run: () => {
textarea.value = view.state.doc.toString();
textarea.form.submit();
return true;
},
},
]);
view.dispatch({
effects: EditorState.appendConfig.of(submitKeymap),
});
}

textarea.parentNode.insertBefore(view.dom, textarea);
textarea.style.display = "none";

// Store reference on textarea for backward compatibility
textarea.codemirrorObject = {
getValue: () => view.state.doc.toString(),
setValue: (text) => {
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: text },
});
},
setLine: (line, text) => {
const lineObj = view.state.doc.line(line + 1);
view.dispatch({
changes: { from: lineObj.from, to: lineObj.to, insert: text },
});
},
save: () => {
textarea.value = view.state.doc.toString();
},
getView: () => view,
};

// Sync value before form submission
if (textarea.form) {
textarea.form.addEventListener("submit", () => {
textarea.value = view.state.doc.toString();
});
textarea.form.addEventListener("jenkins:apply", () => {
textarea.value = view.state.doc.toString();
});
}

return view;
}

function init() {
// Handle textareas with codemirror-mode attribute (from f:textarea)
behaviorShim.specify(
"TEXTAREA.codemirror",
"codemirror-textarea",
0,
(textarea) => {
const mode = textarea.getAttribute("codemirror-mode") || "text/x-groovy";
let config = {};
const configAttr = textarea.getAttribute("codemirror-config");
if (configAttr) {
try {
config = JSON.parse("{" + configAttr + "}");
} catch {
const match = configAttr.match("^mode: ?'([^']+)'$");
if (match) {
config = { mode: match[1] };
}
}
}
createEditor(textarea, {
mode: config.mode || mode,
lineNumbers: false,
});
},
);

// Handle Script Console textareas
behaviorShim.specify(
"TEXTAREA.script",
"codemirror-script-console",
0,
(textarea) => {
const mode = textarea.getAttribute("script-mode") || "text/x-groovy";
createEditor(textarea, {
mode: mode,
lineNumbers: true,
});
},
);
}

export default { init };
Loading