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
JavaScript
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
};
}