[SNOLAB] Selection expander

Shift+Alt+Right/Left to Expand/Shirink Selection to parent elements. (vise versa) just like vscode

// ==UserScript==
// @name            [SNOLAB] Selection expander
// @name:zh         [SNOLAB] 选区扩展器
// @name:ja         [SNOLAB] 選択範囲拡張ツール
// @name:ko         [SNOLAB] 선택 영역 확장기
// @name:es         [SNOLAB] Expansor de selección
// @name:fr         [SNOLAB] Expandeur de sélection
// @name:de         [SNOLAB] Auswahlerweiterer
// @name:ru         [SNOLAB] Расширитель выделения
// @name:pt         [SNOLAB] Expansor de seleção
// @name:it         [SNOLAB] Espansore di selezione
// @namespace       [email protected]
// @version         0.0.4
// @description     Shift+Alt+Right/Left to Expand/Shirink Selection to parent elements. (vise versa) just like vscode
// @description:zh  Shift+Alt+Right/Left to 扩大/缩小 文字选区,常用于代码复制等操作(反之也可)。 just like vscode
// @description:ja  Shift+Alt+Right/Leftで選択範囲を親要素まで拡大/縮小します。VSCodeのような操作です
// @description:ko  Shift+Alt+Right/Left로 선택 영역을 부모 요소까지 확장/축소합니다. VSCode와 같은 기능입니다
// @description:es  Shift+Alt+Right/Left para expandir/contraer la selección a elementos padre. Como en vscode
// @description:fr  Shift+Alt+Right/Left pour étendre/réduire la sélection aux éléments parents. Comme dans vscode
// @description:de  Shift+Alt+Right/Left zum Erweitern/Verkleinern der Auswahl auf übergeordnete Elemente. Wie in vscode
// @description:ru  Shift+Alt+Right/Left для расширения/сжатия выделения до родительских элементов. Как в vscode
// @description:pt  Shift+Alt+Right/Left para expandir/contrair a seleção para elementos pai. Como no vscode
// @description:it  Shift+Alt+Right/Left per espandere/contrarre la selezione agli elementi padre. Come in vscode
// @author          snomiao
// @match           *://*/*
// @grant           none
// ==/UserScript==
// note: migrated from https://gist.github.com/snomiao/7e4d17e1b618167654c4d1ae0dc23cd3f

globalThis?.selectionExpander?.unload?.();

function hotkeyMatch(event, hotkey) {
    if (Boolean(event.altKey) !== /alt|alter/i.test(hotkey)) return false;
    if (Boolean(event.ctrlKey) !== /ctrl|control/i.test(hotkey)) return false;
    if (Boolean(event.metaKey) !== /meta|win|cmd/i.test(hotkey)) return false;
    if (Boolean(event.shiftKey) !== /shift/i.test(hotkey)) return false;
    const key = hotkey.replace(/alt|alter|ctrl|control|meta|win|cmd|shift/gi, "");
    if (!key.toLowerCase().match(event.key.toLowerCase())) return false;
    event.preventDefault();
    return true;
}
const coreState = { sel: null };
const expander = ([a, b]) => {
    // expand to sibling or parent
    const ap = a.previousSibling || a.parentNode;
    const bp = b.nextSibling || b.parentNode;
    if (ap?.contains(bp)) return [ap, ap];
    if (bp?.contains(ap)) return [bp, bp];
    if (ap && bp) return [ap, bp];
    return null;
};
const fnLister = (fn, val) => {
    const out = fn(val);
    return out ? [out, ...fnLister(fn, out)] : [];
};
const expanderLister = ([a, b]) => {
    return fnLister(expander, [a, b]);
};

function updateCoreStateAndGetSel() {
    const sel = globalThis.getSelection();
    const { anchorNode, focusNode, anchorOffset, focusOffset } = sel;
    if (!coreState.sel) {
        coreState.sel = { anchorNode, focusNode, anchorOffset, focusOffset };
    }
    const coreNodes = [coreState.sel.anchorNode, coreState.sel.focusNode];
    if (!coreNodes.every((node) => sel.containsNode(node))) {
        coreState.sel = { anchorNode, focusNode, anchorOffset, focusOffset };
    }
    return { sel, coreNodes };
}
function selectionExpand() {
    const { sel } = updateCoreStateAndGetSel();
    // expand
    const expand = expander([sel.anchorNode, sel.focusNode]);
    if (!expand) return; // can't expand anymore
    const [anc, foc] = expand;
    sel.setBaseAndExtent(anc, 0, foc, foc.childNodes.length);
}
function selectionShirink() {
    const { sel, coreNodes } = updateCoreStateAndGetSel();
    const list = expanderLister(coreNodes);
    const rangeNodes = list
        .reverse()
        .find((rangeNodes) => rangeNodes.every((node) => sel.containsNode(node)));
    if (rangeNodes) {
        const [a, b] = rangeNodes;
        sel.setBaseAndExtent(a, 0, b, b.childNodes.length);
        return;
    }
    const { anchorNode, focusNode, anchorOffset, focusOffset } = coreState.sel;
    if (!sel.containsNode(anchorNode)) {
        sel.collapseToStart();
        return;
    }
    sel.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
}

const handleKeydown = (event) => {
    if (hotkeyMatch(event, "alt+shift+arrowright")) selectionExpand();
    if (hotkeyMatch(event, "alt+shift+arrowleft")) selectionShirink();
};

// load
globalThis.addEventListener("keydown", handleKeydown);
const unload = () => {
    globalThis.removeEventListener("keydown", handleKeydown);
};

// export unload
globalThis.selectionExpander = { unload };