mirror of
https://github.com/penpot/penpot-mcp.git
synced 2026-04-25 11:18:37 +00:00
Improve REPL
- Add full command history navigation with arrow up/down keys - Show history indicator while browsing (yellow box with position) - Auto-resize textarea to fit content - Improve scroll behavior to show output after execution - Remove redundant auto-fill of previous code - Streamline page layout (remove heading, fix scrollbar issues)
This commit is contained in:
parent
9903bdf2dd
commit
d3e531ec7c
@ -6,18 +6,21 @@
|
||||
<title>Penpot API REPL</title>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
font-family: "Consolas", "Monaco", "Lucida Console", monospace;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
body {
|
||||
font-family: "Consolas", "Monaco", "Lucida Console", monospace;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.repl-container {
|
||||
@ -25,9 +28,12 @@
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
min-height: 400px;
|
||||
max-height: 80vh;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.repl-entry {
|
||||
@ -55,7 +61,7 @@
|
||||
|
||||
.code-input {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
min-height: 80px;
|
||||
font-family: "Consolas", "Monaco", "Lucida Console", monospace;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ddd;
|
||||
@ -159,8 +165,9 @@
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: 15px;
|
||||
padding: 10px 0;
|
||||
font-style: italic;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.entry-number {
|
||||
@ -168,37 +175,52 @@
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.history-indicator {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
color: #856404;
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Penpot API REPL</h1>
|
||||
|
||||
<div class="repl-container" id="repl-container">
|
||||
<!-- REPL entries will be dynamically added here -->
|
||||
</div>
|
||||
|
||||
<div class="controls-hint">Press Ctrl+Enter to execute code • Shift+Enter for new line</div>
|
||||
<div class="controls-hint">Ctrl+Enter to execute • Arrow up/down for command history</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
let isExecuting = false;
|
||||
let entryCounter = 1;
|
||||
let lastCode = ""; // store the last executed code
|
||||
let commandHistory = []; // full history of executed commands
|
||||
let historyIndex = 0; // current position in history
|
||||
let isBrowsingHistory = false; // whether we are currently browsing history
|
||||
let tempInput = ""; // temporary storage for current input when browsing history
|
||||
|
||||
// create the initial input entry
|
||||
createNewEntry();
|
||||
|
||||
function createNewEntry() {
|
||||
const entryId = `entry-${entryCounter}`;
|
||||
const defaultCode = lastCode || "";
|
||||
const isFirstEntry = entryCounter === 1;
|
||||
const placeholder = isFirstEntry
|
||||
? `// Enter your JavaScript code here...
|
||||
console.log('Hello from Penpot!');
|
||||
return 'This will be the result';`
|
||||
: "";
|
||||
const entryHtml = `
|
||||
<div class="repl-entry" id="${entryId}">
|
||||
<div class="entry-number">In [${entryCounter}]:</div>
|
||||
<div class="input-section">
|
||||
<textarea class="code-input" id="code-input-${entryCounter}"
|
||||
placeholder="// Enter your JavaScript code here...
|
||||
console.log('Hello from Penpot!');
|
||||
return 'This will be the result';">${escapeHtml(defaultCode)}</textarea>
|
||||
placeholder="${placeholder}"></textarea>
|
||||
<button class="execute-btn" id="execute-btn-${entryCounter}">Execute Code</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -209,18 +231,163 @@ return 'This will be the result';">${escapeHtml(defaultCode)}</textarea>
|
||||
// bind events for this entry
|
||||
bindEntryEvents(entryCounter);
|
||||
|
||||
// focus on the new input
|
||||
$(`#code-input-${entryCounter}`).focus();
|
||||
// focus on the new input without scrolling
|
||||
const $input = $(`#code-input-${entryCounter}`);
|
||||
$input[0].focus({ preventScroll: true });
|
||||
|
||||
// auto-resize textarea
|
||||
$(`#code-input-${entryCounter}`).on("input", function () {
|
||||
this.style.height = "auto";
|
||||
this.style.height = Math.max(60, this.scrollHeight) + "px";
|
||||
// auto-resize textarea on input
|
||||
$input.on("input", function () {
|
||||
autoResizeTextarea(this);
|
||||
});
|
||||
|
||||
entryCounter++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes a textarea to fit its content, with a minimum height.
|
||||
* Adds border height since scrollHeight excludes borders but box-sizing: border-box includes them.
|
||||
*/
|
||||
function autoResizeTextarea(textarea) {
|
||||
textarea.style.height = "auto";
|
||||
// add 2px for top and bottom border (1px each)
|
||||
textarea.style.height = Math.max(80, textarea.scrollHeight + 2) + "px";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cursor is at the beginning of a textarea (position 0 with no selection).
|
||||
*/
|
||||
function isCursorAtBeginning(textarea) {
|
||||
return textarea.selectionStart === 0 && textarea.selectionEnd === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cursor is at the end of a textarea (position at text length with no selection).
|
||||
*/
|
||||
function isCursorAtEnd(textarea) {
|
||||
const len = textarea.value.length;
|
||||
return textarea.selectionStart === len && textarea.selectionEnd === len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates through command history for the given entry's textarea.
|
||||
* @param direction -1 for previous (up), +1 for next (down)
|
||||
* @param entryNum the entry number
|
||||
*/
|
||||
function navigateHistory(direction, entryNum) {
|
||||
const $codeInput = $(`#code-input-${entryNum}`);
|
||||
const textarea = $codeInput[0];
|
||||
|
||||
if (commandHistory.length === 0) return;
|
||||
|
||||
if (direction === -1) {
|
||||
// going back in history (arrow up)
|
||||
if (!isBrowsingHistory) {
|
||||
// starting to browse history: save current input
|
||||
tempInput = $codeInput.val();
|
||||
isBrowsingHistory = true;
|
||||
historyIndex = commandHistory.length - 1;
|
||||
} else if (historyIndex > 0) {
|
||||
// go further back in history
|
||||
historyIndex--;
|
||||
} else {
|
||||
// already at oldest entry, do nothing
|
||||
return;
|
||||
}
|
||||
$codeInput.val(commandHistory[historyIndex]);
|
||||
autoResizeTextarea(textarea);
|
||||
// keep cursor at beginning for continued history navigation
|
||||
textarea.setSelectionRange(0, 0);
|
||||
// show history position (1 = most recent)
|
||||
const position = commandHistory.length - historyIndex;
|
||||
showHistoryIndicator(entryNum, position, commandHistory.length);
|
||||
} else {
|
||||
// going forward in history (arrow down)
|
||||
if (!isBrowsingHistory) {
|
||||
// not browsing history, do nothing
|
||||
return;
|
||||
} else if (historyIndex >= commandHistory.length - 1) {
|
||||
// at most recent entry, return to original input
|
||||
isBrowsingHistory = false;
|
||||
$codeInput.val(tempInput);
|
||||
autoResizeTextarea(textarea);
|
||||
// cursor at beginning (same as when we entered history)
|
||||
textarea.setSelectionRange(0, 0);
|
||||
hideHistoryIndicator();
|
||||
} else {
|
||||
// go forward in history
|
||||
historyIndex++;
|
||||
$codeInput.val(commandHistory[historyIndex]);
|
||||
autoResizeTextarea(textarea);
|
||||
// keep cursor at beginning
|
||||
textarea.setSelectionRange(0, 0);
|
||||
// update history position indicator
|
||||
const position = commandHistory.length - historyIndex;
|
||||
showHistoryIndicator(entryNum, position, commandHistory.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exits history browsing mode, keeping current content in the input.
|
||||
* Moves cursor to end of input.
|
||||
* @param entryNum the entry number (optional, cursor not moved if not provided)
|
||||
*/
|
||||
function exitHistoryBrowsing(entryNum) {
|
||||
if (isBrowsingHistory) {
|
||||
isBrowsingHistory = false;
|
||||
hideHistoryIndicator();
|
||||
if (entryNum !== undefined) {
|
||||
const textarea = $(`#code-input-${entryNum}`)[0];
|
||||
const len = textarea.value.length;
|
||||
textarea.setSelectionRange(len, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the repl container to show the output section of the given entry.
|
||||
*/
|
||||
function scrollToOutput($entry) {
|
||||
const $container = $("#repl-container");
|
||||
const $outputSection = $entry.find(".output-section");
|
||||
if ($outputSection.length) {
|
||||
const containerTop = $container.offset().top;
|
||||
const outputTop = $outputSection.offset().top;
|
||||
const scrollTop = $container.scrollTop();
|
||||
$container.animate(
|
||||
{
|
||||
scrollTop: scrollTop + (outputTop - containerTop),
|
||||
},
|
||||
300
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or updates the history indicator for the current entry.
|
||||
* @param entryNum the entry number
|
||||
* @param position 1-based position from most recent (1 = most recent)
|
||||
* @param total total number of history items
|
||||
*/
|
||||
function showHistoryIndicator(entryNum, position, total) {
|
||||
const $entry = $(`#entry-${entryNum}`);
|
||||
let $indicator = $entry.find(".history-indicator");
|
||||
|
||||
if ($indicator.length === 0) {
|
||||
$entry.find(".input-section").before('<div class="history-indicator"></div>');
|
||||
$indicator = $entry.find(".history-indicator");
|
||||
}
|
||||
|
||||
$indicator.text(`History item ${position}/${total}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the history indicator.
|
||||
*/
|
||||
function hideHistoryIndicator() {
|
||||
$(".history-indicator").remove();
|
||||
}
|
||||
|
||||
function bindEntryEvents(entryNum) {
|
||||
const $executeBtn = $(`#execute-btn-${entryNum}`);
|
||||
const $codeInput = $(`#code-input-${entryNum}`);
|
||||
@ -228,11 +395,36 @@ return 'This will be the result';">${escapeHtml(defaultCode)}</textarea>
|
||||
// bind execute button click
|
||||
$executeBtn.on("click", () => executeCode(entryNum));
|
||||
|
||||
// bind Ctrl+Enter keyboard shortcut
|
||||
// bind keyboard shortcuts
|
||||
$codeInput.on("keydown", function (e) {
|
||||
// Ctrl+Enter to execute
|
||||
if (e.ctrlKey && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
exitHistoryBrowsing(entryNum);
|
||||
executeCode(entryNum);
|
||||
return;
|
||||
}
|
||||
|
||||
// arrow up at beginning of input (or while browsing history): navigate to previous history entry
|
||||
if (e.key === "ArrowUp" && (isBrowsingHistory || isCursorAtBeginning(this))) {
|
||||
e.preventDefault();
|
||||
navigateHistory(-1, entryNum);
|
||||
return;
|
||||
}
|
||||
|
||||
// arrow down at end of input (or while browsing history): navigate to next history entry
|
||||
if (e.key === "ArrowDown" && (isBrowsingHistory || isCursorAtEnd(this))) {
|
||||
e.preventDefault();
|
||||
navigateHistory(+1, entryNum);
|
||||
return;
|
||||
}
|
||||
|
||||
// any key except pure modifier keys exits history browsing
|
||||
if (isBrowsingHistory) {
|
||||
const isModifierOnly = ["Shift", "Control", "Alt", "Meta"].includes(e.key);
|
||||
if (!isModifierOnly) {
|
||||
exitHistoryBrowsing();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -328,15 +520,16 @@ return 'This will be the result';">${escapeHtml(defaultCode)}</textarea>
|
||||
$codeInput.prop("readonly", true);
|
||||
$(`#execute-btn-${entryNum}`).remove();
|
||||
|
||||
// store the code for the next entry
|
||||
lastCode = code;
|
||||
// store the code in history
|
||||
commandHistory.push(code);
|
||||
isBrowsingHistory = false; // reset history navigation
|
||||
tempInput = ""; // clear temporary input
|
||||
|
||||
// create a new entry for the next input
|
||||
createNewEntry();
|
||||
|
||||
// scroll to the new entry
|
||||
const $container = $("#repl-container");
|
||||
$container.scrollTop($container[0].scrollHeight);
|
||||
// scroll to the output section of the executed entry
|
||||
scrollToOutput($entry);
|
||||
},
|
||||
error: function (xhr) {
|
||||
let errorData;
|
||||
@ -346,6 +539,9 @@ return 'This will be the result';">${escapeHtml(defaultCode)}</textarea>
|
||||
errorData = { error: "Network error or invalid response" };
|
||||
}
|
||||
displayResult(entryNum, errorData, true);
|
||||
|
||||
// scroll to the error output
|
||||
scrollToOutput($entry);
|
||||
},
|
||||
complete: function () {
|
||||
setExecuting(entryNum, false);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user