UNPKG

shahnevis-core

Version:

**Shahnevis Core** is a lightweight and flexible library for building custom code editors. It provides essential features like syntax highlighting, a minimap, multi-cursor support, line numbering, and plugin management. This library is framework-agnostic,

430 lines (397 loc) 19.7 kB
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /* This file is part of Shahnevis Core. Copyright (C) 2024 shahrooz saneidarani (github.com/shahroozD) Shahnevis Core is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Shahnevis Core is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { updateSyntaxHighlighting } from "../core/highlighting"; import { updateMinimapContent } from "../core/minimap"; import { updateIndentationGuides } from "./lineNumbers"; export function setFoldedBlock(foldedBlocks, newMap) { return _objectSpread({}, newMap); } // Function to find block start and end positions for curly-brace-based languages function findBlockBoundaries(startLine, actualLineNumber, code) { var foldedBlocks = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; var lines = foldedBlocks[actualLineNumber] || code.split("\n"); var blockStart = startLine; var openBraces = 0; // console.log(foldedBlocks); if (foldedBlocks[actualLineNumber]) { startLine = foldedBlocks[actualLineNumber] ? 0 : startLine; openBraces = 1; } var blockEnd = -1; var codeLine = actualLineNumber; for (var i = startLine; i < lines.length; i++) { var _foldedBlocks$codeLin; // console.log(foldedBlocks); // console.log("foldedBlocks[i]: ", foldedBlocks[actualLineNumber]); if (lines[i].includes("{") && !((_foldedBlocks$codeLin = foldedBlocks[codeLine]) !== null && _foldedBlocks$codeLin !== void 0 && _foldedBlocks$codeLin.length)) { if (blockStart === -1) blockStart = i; openBraces++; } else { var _foldedBlocks$actualL; // console.log(foldedBlocks[actualLineNumber]?.length); // console.log(typeof(foldedBlocks[actualLineNumber]?.length)); // codeLine += ((_foldedBlocks$actualL = foldedBlocks[actualLineNumber]) === null || _foldedBlocks$actualL === void 0 ? void 0 : _foldedBlocks$actualL.length) || 0; } // console.log("!foldedBlocks[i]: ", !foldedBlocks[codeLine]?.length); // console.log("i: ", i); // console.log("codeLine: ", codeLine); // console.log("lines: ", lines); // console.log(`lines[]: |${lines[i]}|`); // console.log(lines[i].includes("}")); // if (lines[i].includes("}")) { openBraces--; // console.log("openBraces: ", openBraces); if (openBraces === 0) { blockEnd = i; break; } } else {} codeLine++; } // && blockEnd !== -1 if (blockStart !== -1) { return { blockStart: blockStart, blockEnd: blockEnd }; } return null; } // Function to update the fold/unfold symbol based on the block state function updateFoldSymbol(lineIndex) { var folded = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var symbol = !folded ? '▼' : '▶'; var foldingButtons = document.querySelectorAll('.folding-button'); var expandButtons = document.querySelectorAll('.expand-button'); if (expandButtons[lineIndex]) { expandButtons[lineIndex].style.visibility = folded ? 'visible' : 'hidden'; } if (foldingButtons[lineIndex]) { foldingButtons[lineIndex].innerHTML = symbol; } } // Function to find nested blocks and add folding buttons export function foldingButtons(line, index, actualLineNumber, editor, minimapContent, lineNumbers, foldingManager) { var foldedBlocks = foldingManager.getFoldedBlocksById(); // console.log(foldedBlocksById); var foldingButton = document.createElement('span'); foldingButton.className = 'folding-button'; var folded = !!foldedBlocks[actualLineNumber]; if (line.includes("{")) { foldingButton.style.cursor = 'pointer'; foldingButton.innerHTML = folded ? '▶' : '▼'; foldingButton.addEventListener('click', function () { return foldingManager.toggleFold(index, actualLineNumber, editor, minimapContent, lineNumbers, foldingManager); }); } else { foldingButton.style.visibility = 'hidden'; } return foldingButton; } // Function to find nested blocks and add folding buttons export function expandButtons(line, index, actualLineNumber, editor, minimapContent, lineNumbers, foldingManager) { var foldedBlocks = foldingManager.getFoldedBlocksById(); // console.log(foldedBlocksById); var expandBtn = document.createElement('div'); expandBtn.className = 'expand-button'; var folded = !!foldedBlocks[actualLineNumber]; if (line.includes("{")) { expandBtn.innerHTML = '... }'; expandBtn.style.left = "".concat(line.length * 9 + 75, "px"); expandBtn.style.visibility = folded ? 'visible' : 'hidden'; expandBtn.addEventListener('click', function () { return foldingManager.toggleFold(index, actualLineNumber, editor, minimapContent, lineNumbers, foldingManager); }); } else { expandBtn.style.visibility = 'hidden'; } return expandBtn; } function pythonlike_findIndentedBlock(startLine, code) { var lines = code.split("\n"); var indentLevel = lines[startLine].match(/^\s*/)[0].length; var blockEnd = startLine + 1; while (blockEnd < lines.length && lines[blockEnd].match(/^\s*/)[0].length > indentLevel) { blockEnd++; } return { blockStart: startLine, blockEnd: blockEnd - 1 }; } /** * Rebuild the fully‑expanded lines and a view→full mapping * by splicing in every folded block in ascending order of start lines. * * @param {string} viewText * @param {{ [startLine: number]: string[] }} foldedBlocksMap * @returns {{ fullLines: string[], viewToFull: number[] }} */ export function expandViewToFull(viewText, foldedBlocksMap) { // Split the collapsed text into lines var viewLines = viewText.split(/\r?\n/); // Work on a mutable copy for splicing var fullLines = _toConsumableArray(viewLines); // 1) Gather and sort full‑start keys var starts = Object.keys(foldedBlocksMap).map(function (n) { return parseInt(n, 10); }).sort(function (a, b) { return a - b; }); // 2) Build a foldEntries array with viewStart and hidden info var hiddenAccum = 0; var foldEntries = starts.map(function (start) { var hiddenLines = foldedBlocksMap[start] || []; var countHidden = hiddenLines.length; // In the collapsed view, this block header appears at: var viewStart = start - hiddenAccum; hiddenAccum += countHidden; return { viewStart: viewStart, hiddenCount: countHidden, hiddenLines: hiddenLines }; }); // 3) Splice each block’s hidden lines back into fullLines var offset = 0; var _iterator = _createForOfIteratorHelper(foldEntries), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var _step$value = _step.value, viewStart = _step$value.viewStart, hiddenLines = _step$value.hiddenLines; if (hiddenLines.length === 0) continue; // Insert _after_ the block header in fullLines, accounting for prior offsets fullLines.splice.apply(fullLines, [viewStart + 1 + offset, 0].concat(_toConsumableArray(hiddenLines))); offset += hiddenLines.length; } // 4) Build view→full mapping } catch (err) { _iterator.e(err); } finally { _iterator.f(); } var viewToFull = viewLines.map(function (_, vIdx) { var fIdx = vIdx; var _iterator2 = _createForOfIteratorHelper(foldEntries), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var _step2$value = _step2.value, viewStart = _step2$value.viewStart, hiddenCount = _step2$value.hiddenCount; if (viewStart < vIdx) fIdx += hiddenCount;else break; } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } return fIdx; }); return { fullText: fullLines.join("\n"), viewToFull: viewToFull }; } /** * Recursively compute the full span for a folded block that begins at “start”. * The base span is determined by foldedBlocks[start].length (for example, a simple block * may have length 2 meaning it covers two lines). Then any folded block whose key falls * inside the base interval will extend the overall ending line if its own effective end is later. */ export function computeTotalSpan(start, foldedBlocks) { if (!foldedBlocks[start]) return 0; // Use the folded block's own length as the base span. var baseSpan = foldedBlocks[start].length; var totalEnd = start + baseSpan; // Look through every key (sorted in document order) that may be nested. var sortedKeys = Object.keys(foldedBlocks).map(Number).sort(function (a, b) { return a - b; }); var _iterator3 = _createForOfIteratorHelper(sortedKeys), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var k = _step3.value; // If k is nested inside the current block... if (k > start && k < totalEnd) { var nestedSpan = computeTotalSpan(k, foldedBlocks); var nestedEnd = k + nestedSpan; // Extend totalEnd if the nested block reaches further down. if (nestedEnd > totalEnd) { totalEnd = nestedEnd + 1; } } } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } return totalEnd - start; } /** * Swap two folded block segments (with their nested blocks) in the mapping. */ export function updateFoldedBlocksAfterSwap(foldedBlocks, start1, start2) { // Determine lower starting lines. var A = Math.min(start1, start2); var spanA = computeTotalSpan(A, foldedBlocks); // Compute effective spans for each block. var groupA_end = A + spanA; // end boundary for group A // Determine higher starting lines. var B = Math.max(start1, start2) + (start2 > start1 ? spanA : 0); // const B = Math.max(start1, start2) + spanA; var spanB = computeTotalSpan(B, foldedBlocks); // Compute effective spans for each block. var groupB_end = B + spanB; // end boundary for group B var updatedFoldedBlocks = {}; for (var key in foldedBlocks) { var numKey = parseInt(key, 10); var newKey = numKey; var _groupA_end = A + spanA; // end boundary for group A if (numKey >= _groupA_end && numKey < B) { // Keys in the gap between the two blocks. // Their new key shifts by the difference (spanB - spanA). newKey = numKey - (A + spanA) + (A + spanB) - 1; } else if (numKey >= A && numKey < _groupA_end) { // Keys that belong to group A (first block) // They will be relocated to after group B. newKey = numKey - A + (B + spanB - spanA); } else if (numKey >= B && numKey < groupB_end) { // Keys that belong to group B (second block) // They move to the top (starting at A). newKey = numKey - B + A; } else { // Other keys (before A or after group B) remain unchanged. newKey = numKey; } updatedFoldedBlocks[newKey] = foldedBlocks[key]; } return updatedFoldedBlocks; } // Function to handle the folding state update export function updateFoldingState(changeInfo, editor, oldFoldedBlocks, minimapContent, lineNumbers, foldingManager) { // Destructure key values from the change object. // const { changeType, startLine: changeStart, endLine: changeEnd, startPos, lineCountChange, logicalCountChange: changeLength, data } = changeInfo; var changeType = changeInfo.changeType, changeStart = changeInfo.startLine, changeEnd = changeInfo.endLine, startPos = changeInfo.startPos, lineCountChange = changeInfo.lineCountChange, changeLength = changeInfo.logicalCountChange, data = changeInfo.data; // No net change → keep existing folds if (lineCountChange === 0) { return oldFoldedBlocks; } var newFoldedBlocks = {}; // This variable will track the cumulative net shift due to unfolded blocks, // which affects the starting line numbers of subsequent folded blocks. // let cumulativeShift = changeLength; var inputShift = changeLength > 0 ? lineCountChange : changeLength; var cumulativeShift = changeLength > 0 ? lineCountChange : changeLength; // Process each folded block from the old state. for (var key in oldFoldedBlocks) { // Convert key to an integer line number. var blockStart = parseInt(key, 10); var foldedBlock = oldFoldedBlocks[key]; var blockLength = foldedBlock.length; var blockEnd = blockStart + blockLength - 1; // If the change is inserting a newline and it occurs inside a folded block, then "open" the block. if (changeStart === blockStart && lineCountChange > 0) { // Insertion on the first line of the folded block: // Unfold the block visually/logically. var keys = Object.keys(oldFoldedBlocks); var idx = keys.indexOf(String(key)); var keysBefore = keys.slice(0, idx); var dataBefore = keysBefore.map(function (k) { return oldFoldedBlocks[k]; }); var totalShift = dataBefore.flat().length; foldingManager.toggleFold(blockStart - totalShift, blockStart, editor, minimapContent, lineNumbers, foldingManager); editor.selectionStart = editor.selectionEnd = startPos; // Since we open the block, we remove it and only count the inserted lines. cumulativeShift += blockLength; continue; } if (changeStart >= blockStart && changeStart + lineCountChange <= blockStart) { // if (changeStart >= blockStart && changeStart <= blockEnd) { // The block is being modified by an enter; remove it from the folded stack. continue; } if (changeType == "deletion" && changeStart <= blockStart && changeEnd >= blockEnd) { continue; } // Otherwise, if the block starts before the change, it remains unchanged. // If it starts at or after the change, shift its starting line by the net line change. var newKey = blockStart < changeStart ? blockStart : blockStart + inputShift; if (newKey >= 0) newFoldedBlocks[newKey] = foldedBlock; } return newFoldedBlocks; } export function toggleFold(startLine, actualLineNumber, editor, minimapContent, lineNumbers, foldingManager) { var foldedBlocks = foldingManager.getFoldedBlocksById(); var code = editor.value; var block = findBlockBoundaries(startLine, actualLineNumber, code, foldedBlocks); var updatedBlocks = {}; // console.log(block); // console.log("actualLineNumber: ", actualLineNumber); if (block) { var blockStart = block.blockStart, blockEnd = block.blockEnd; if (foldedBlocks[actualLineNumber]) { var foldedCode = foldedBlocks[actualLineNumber]; var unfoldedCode = editor.value.split("\n"); unfoldedCode.splice.apply(unfoldedCode, [blockStart + 1, 0].concat(_toConsumableArray(foldedCode))); editor.value = unfoldedCode.join("\n"); // console.log(unfoldedCode); updatedBlocks = _objectSpread({}, foldedBlocks); delete updatedBlocks[actualLineNumber]; // Remove the old block // updateIndentationGuides(editor, minimapContent, lineNumbers, {getFoldedBlocksById, foldingButtons, expandButtons}); updateFoldSymbol(blockStart, false); } else { var codeLines = editor.value.split("\n"); var foldedLines = codeLines.splice(blockStart + 1, blockEnd - blockStart); updatedBlocks = _objectSpread(_objectSpread({}, foldedBlocks), {}, _defineProperty({}, actualLineNumber, foldedLines)); codeLines.splice(blockStart + 1, 0); // console.log(codeLines); editor.value = codeLines.join("\n"); // updateIndentationGuides(editor, minimapContent, lineNumbers, {getFoldedBlocksById, foldingButtons, expandButtons}); updateFoldSymbol(blockStart, true); } } else { updatedBlocks = _objectSpread({}, foldedBlocks); } return { newCode: editor.value, newFoldedBlocks: updatedBlocks }; }