| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947 |
- /* global $ $$ $create $remove focusAccessibility toggleDataset */// dom.js
- /* global CodeMirror */
- /* global chromeLocal */// storage-util.js
- /* global colorMimicry */
- /* global debounce stringAsRegExp tryRegExp */// toolbox.js
- /* global editor */
- /* global t */// localization.js
- 'use strict';
- (() => {
- require(['/edit/global-search.css']);
- //region Constants and state
- const INCREMENTAL_SEARCH_DELAY = 0;
- const ANNOTATE_SCROLLBAR_DELAY = 350;
- const ANNOTATE_SCROLLBAR_OPTIONS = {maxMatches: 10e3};
- const STORAGE_UPDATE_DELAY = 500;
- const DIALOG_SELECTOR = '#search-replace-dialog';
- const DIALOG_STYLE_SELECTOR = '#search-replace-dialog-style';
- const TARGET_CLASS = 'search-target-editor';
- const MATCH_CLASS = 'search-target-match';
- const MATCH_TOKEN_NAME = 'searching';
- const APPLIES_VALUE_CLASS = 'applies-value';
- const RX_MAYBE_REGEXP = /^\s*\/(.+?)\/([simguy]*)\s*$/;
- const state = {
- firstRun: true,
- // used for case-sensitive matching directly
- find: '',
- // used when /re/ is detected or for case-insensitive matching
- rx: null,
- // used by overlay and doSearchInApplies, equals to rx || stringAsRegExp(find)
- rx2: null,
- icase: true,
- reverse: false,
- lastFind: '',
- numFound: 0,
- numApplies: -1,
- replace: '',
- lastReplace: null,
- cm: null,
- input: null,
- input2: null,
- dialog: null,
- tally: null,
- originalFocus: null,
- undoHistory: [],
- searchInApplies: !document.documentElement.classList.contains('usercss'),
- };
- //endregion
- //region Events
- const ACTIONS = {
- key: {
- 'Enter': event => {
- switch (document.activeElement) {
- case state.input:
- if (state.dialog.dataset.type === 'find') {
- const found = doSearch({canAdvance: false});
- if (found) {
- const target = $('.' + TARGET_CLASS);
- const cm = target.CodeMirror;
- /* Since this runs in `keydown` event we have to delay focusing
- * to prevent CodeMirror from seeing and handling the key */
- setTimeout(() => (cm || target).focus());
- if (cm) {
- const {from, to} = cm.state.search.searchPos;
- cm.jumpToPos(from, to);
- }
- }
- destroyDialog({restoreFocus: !found});
- return;
- }
- // fallthrough
- case state.input2:
- doReplace();
- return;
- }
- return !focusAccessibility.closest(event.target);
- },
- 'Esc': () => {
- destroyDialog({restoreFocus: true});
- },
- },
- click: {
- next: () => doSearch({reverse: false}),
- prev: () => doSearch({reverse: true}),
- close: () => destroyDialog({restoreFocus: true}),
- replace: () => doReplace(),
- replaceAll: () => doReplaceAll(),
- undo: () => doUndo(),
- clear() {
- setInputValue(this._input, '');
- },
- case() {
- state.icase = !state.icase;
- state.lastFind = '';
- toggleDataset(this, 'enabled', !state.icase);
- doSearch({canAdvance: false});
- },
- },
- };
- const EVENTS = {
- oninput() {
- state.find = state.input.value;
- debounce(doSearch, INCREMENTAL_SEARCH_DELAY, {canAdvance: false});
- adjustTextareaSize(this);
- if (!state.find) enableReplaceButtons(false);
- },
- onkeydown(event) {
- const action = ACTIONS.key[CodeMirror.keyName(event)];
- if (action && action(event) !== false) {
- event.preventDefault();
- }
- },
- onclick(event) {
- const el = event.target.closest('[data-action]');
- const action = el && ACTIONS.click[el.dataset.action];
- if (action && action.call(el, event) !== false) {
- event.preventDefault();
- }
- },
- onfocusout() {
- if (!state.dialog.contains(document.activeElement)) {
- state.dialog.on('focusin', EVENTS.onfocusin);
- state.dialog.off('focusout', EVENTS.onfocusout);
- }
- },
- onfocusin() {
- state.dialog.on('focusout', EVENTS.onfocusout);
- state.dialog.off('focusin', EVENTS.onfocusin);
- trimUndoHistory();
- enableUndoButton(state.undoHistory.length);
- if (state.find) doSearch({canAdvance: false});
- },
- };
- const DIALOG_PROPS = {
- dialog: {
- onclick: EVENTS.onclick,
- onkeydown: EVENTS.onkeydown,
- },
- input: {
- oninput: EVENTS.oninput,
- },
- input2: {
- oninput() {
- state.replace = this.value;
- adjustTextareaSize(this);
- debounce(writeStorage, STORAGE_UPDATE_DELAY);
- },
- },
- };
- //endregion
- //region Commands
- const COMMANDS = {
- find(cm, {reverse = false} = {}) {
- state.reverse = reverse;
- focusDialog('find', cm);
- },
- findNext: cm => doSearch({reverse: false, cm}),
- findPrev: cm => doSearch({reverse: true, cm}),
- replace(cm) {
- state.reverse = false;
- focusDialog('replace', cm);
- },
- };
- COMMANDS.replaceAll = COMMANDS.replace;
- //endregion
- Object.assign(CodeMirror.commands, COMMANDS);
- readStorage();
- //region Find
- function initState({initReplace} = {}) {
- const text = state.find;
- const textChanged = text !== state.lastFind;
- if (textChanged) {
- state.numFound = 0;
- state.numApplies = -1;
- state.lastFind = text;
- const match = text && text.match(RX_MAYBE_REGEXP);
- const unicodeFlag = 'unicode' in RegExp.prototype ? 'u' : '';
- const string2regexpFlags = (state.icase ? 'gi' : 'g') + unicodeFlag;
- state.rx = match && tryRegExp(match[1], 'g' + match[2].replace(/[guy]/g, '') + unicodeFlag) ||
- text && (state.icase || text.includes('\n')) && stringAsRegExp(text, string2regexpFlags);
- state.rx2 = state.rx || text && stringAsRegExp(text, string2regexpFlags);
- state.cursorOptions = {
- caseFold: !state.rx && state.icase,
- multiline: true,
- };
- debounce(writeStorage, STORAGE_UPDATE_DELAY);
- }
- if (initReplace && state.replace !== state.lastReplace) {
- state.lastReplace = state.replace;
- state.replaceValue = state.replace.replace(/(\\r)?\\n/g, '\n').replace(/\\t/g, '\t');
- state.replaceHasRefs = /\$[$&`'\d]/.test(state.replaceValue);
- }
- const cmFocused = document.activeElement && document.activeElement.closest('.CodeMirror');
- state.activeAppliesTo = $(`.${APPLIES_VALUE_CLASS}:focus, .${APPLIES_VALUE_CLASS}.${TARGET_CLASS}`);
- state.cmStart = editor.closestVisible(
- cmFocused && document.activeElement ||
- state.activeAppliesTo ||
- state.cm);
- const cmExtra = $('body > :not(#sections) .CodeMirror');
- state.editors = cmExtra ? [cmExtra.CodeMirror] : editor.getEditors();
- }
- function doSearch({
- reverse = state.reverse,
- canAdvance = true,
- inApplies = true,
- cm,
- } = {}) {
- if (cm) setActiveEditor(cm);
- state.reverse = reverse;
- if (!state.find && !dialogShown()) {
- focusDialog('find', state.cm);
- return;
- }
- initState();
- const {cmStart} = state;
- const {index, found, foundInCode} = state.find && doSearchInEditors({cmStart, canAdvance, inApplies}) || {};
- if (!foundInCode) clearMarker();
- if (!found) makeTargetVisible(null);
- const radiateFrom = foundInCode ? index : state.editors.indexOf(cmStart);
- setupOverlay(radiateArray(state.editors, radiateFrom));
- enableReplaceButtons(foundInCode);
- if (state.find) {
- const firstSuccessfulSearch = foundInCode && !state.numFound;
- debounce(showTally, 0, firstSuccessfulSearch ? 1 : undefined);
- } else {
- showTally(0, 0);
- }
- state.firstRun = false;
- return found;
- }
- function doSearchInEditors({cmStart, canAdvance, inApplies}) {
- const query = state.rx || state.find;
- const reverse = state.reverse;
- const BOF = {line: 0, ch: 0};
- const EOF = getEOF(cmStart);
- const start = state.editors.indexOf(cmStart);
- const total = state.editors.length;
- let i = 0;
- let wrapAround = 0;
- let pos, index, cm;
- if (inApplies && state.activeAppliesTo) {
- if (doSearchInApplies(state.cmStart, canAdvance)) {
- return {found: true};
- }
- if (reverse) pos = EOF; else i++;
- } else {
- pos = getContinuationPos({cm: cmStart, reverse: !canAdvance || reverse});
- wrapAround = !reverse ?
- CodeMirror.cmpPos(pos, BOF) > 0 :
- CodeMirror.cmpPos(pos, EOF) < 0;
- }
- for (; i < total + wrapAround; i++) {
- index = (start + i * (reverse ? -1 : 1) + total) % total;
- cm = state.editors[index];
- if (i) {
- pos = !reverse ? BOF : {line: cm.doc.size, ch: 0};
- }
- const cursor = cm.getSearchCursor(query, pos, state.cursorOptions);
- if (cursor.find(reverse)) {
- makeMatchVisible(cm, cursor);
- return {found: true, foundInCode: true, index};
- }
- const cmForNextApplies = !reverse ? cm : state.editors[index ? index - 1 : total - 1];
- if (inApplies && doSearchInApplies(cmForNextApplies)) {
- return {found: true};
- }
- }
- }
- function doSearchInApplies(cm, canAdvance) {
- if (!state.searchInApplies) return;
- const inputs = editor.getSearchableInputs(cm);
- if (state.reverse) inputs.reverse();
- inputs.splice(0, inputs.indexOf(state.activeAppliesTo));
- for (const input of inputs) {
- const value = input.value;
- if (input === state.activeAppliesTo) {
- state.rx2.lastIndex = input.selectionStart + canAdvance;
- } else {
- state.rx2.lastIndex = 0;
- }
- const match = state.rx2.exec(value);
- if (!match) {
- continue;
- }
- const end = match.index + match[0].length;
- // scroll selected part into view in long inputs,
- // works only outside of current event handlers chain, hence timeout=0
- setTimeout(() => {
- input.setSelectionRange(end, end);
- input.setSelectionRange(match.index, end);
- });
- const canFocus = !state.dialog || !state.dialog.contains(document.activeElement);
- makeTargetVisible(!canFocus && input);
- editor.scrollToEditor(cm);
- if (canFocus) input.focus();
- state.cm = cm;
- clearMarker();
- return true;
- }
- }
- //endregion
- //region Replace
- function doReplace() {
- initState({initReplace: true});
- const cm = state.cmStart;
- const generation = cm.changeGeneration();
- const pos = getContinuationPos({cm, reverse: true});
- const cursor = doReplaceInEditor({cm, pos});
- if (!cursor) {
- return;
- }
- if (cursor.findNext()) {
- clearMarker();
- makeMatchVisible(cm, cursor);
- } else {
- doSearchInEditors({cmStart: getNextEditor(cm)});
- }
- getStateSafe(cm).unclosedOp = false;
- if (cm.curOp) cm.endOperation();
- if (cursor) {
- state.undoHistory.push([[cm, generation]]);
- enableUndoButton(true);
- }
- }
- function doReplaceAll() {
- initState({initReplace: true});
- clearMarker();
- const generations = new Map(state.editors.map(cm => [cm, cm.changeGeneration()]));
- const found = state.editors.filter(cm => doReplaceInEditor({cm, all: true}));
- if (found.length) {
- state.lastFind = null;
- state.undoHistory.push(found.map(cm => [cm, generations.get(cm)]));
- enableUndoButton(true);
- doSearch({canAdvance: false});
- }
- }
- function doReplaceInEditor({cm, pos, all = false}) {
- const cursor = cm.getSearchCursor(state.rx || state.find, pos, state.cursorOptions);
- const replace = state.replaceValue;
- let found;
- cursor.find();
- while (cursor.atOccurrence) {
- found = true;
- if (!cm.curOp) {
- cm.startOperation();
- getStateSafe(cm).unclosedOp = true;
- }
- if (state.rx) {
- const text = cm.getRange(cursor.pos.from, cursor.pos.to);
- cursor.replace(state.replaceHasRefs ? text.replace(state.rx, replace) : replace);
- } else {
- cursor.replace(replace);
- }
- if (!all) {
- makeMatchVisible(cm, cursor);
- return cursor;
- }
- cursor.findNext();
- }
- if (all) {
- getStateSafe(cm).searchPos = null;
- }
- return found;
- }
- function doUndo() {
- let undoneSome;
- saveWindowScrollPos();
- for (const [cm, generation] of state.undoHistory.pop() || []) {
- if (document.body.contains(cm.display.wrapper) && !cm.isClean(generation)) {
- cm.undo();
- cm.getAllMarks().forEach(marker =>
- marker !== state.marker &&
- marker.className === MATCH_CLASS &&
- marker.clear());
- undoneSome = true;
- }
- }
- enableUndoButton(state.undoHistory.length);
- if (state.undoHistory.length) {
- focusUndoButton();
- } else {
- state.input.focus();
- }
- if (undoneSome) {
- state.lastFind = null;
- restoreWindowScrollPos();
- doSearch({
- reverse: false,
- canAdvance: false,
- inApplies: false,
- });
- }
- }
- //endregion
- //region Overlay
- function setupOverlay(queue, debounced) {
- if (!queue.length) {
- return;
- }
- if (queue.length > 1 || !debounced) {
- debounce(setupOverlay, 0, queue, true);
- if (!debounced) {
- return;
- }
- }
- let canContinue = true;
- while (queue.length && canContinue) {
- const cm = queue.shift();
- if (!document.body.contains(cm.display.wrapper)) {
- continue;
- }
- const cmState = getStateSafe(cm);
- const query = state.rx2;
- if ((cmState.overlay || {}).query === query) {
- if (cmState.unclosedOp && cm.curOp) cm.endOperation();
- cmState.unclosedOp = false;
- continue;
- }
- if (cmState.overlay) {
- if (!cm.curOp) cm.startOperation();
- cm.removeOverlay(cmState.overlay);
- cmState.overlay = null;
- canContinue = false;
- }
- const hasMatches = query && cm.getSearchCursor(query, null, state.cursorOptions).find();
- if (hasMatches) {
- if (!cm.curOp) cm.startOperation();
- cmState.overlay = {
- query,
- token: tokenize,
- numFound: 0,
- tallyShownTime: 0,
- };
- cm.addOverlay(cmState.overlay);
- canContinue = false;
- }
- if (cmState.annotate) {
- if (!cm.curOp) cm.startOperation();
- cmState.annotate.clear();
- cmState.annotate = null;
- canContinue = false;
- }
- if (cmState.annotateTimer) {
- clearTimeout(cmState.annotateTimer);
- cmState.annotateTimer = 0;
- }
- if (hasMatches) {
- cmState.annotateTimer = setTimeout(annotateScrollbar, ANNOTATE_SCROLLBAR_DELAY,
- cm, query, state.icase);
- }
- cmState.unclosedOp = false;
- if (cm.curOp) cm.endOperation();
- }
- if (!queue.length) debounce.unregister(setupOverlay);
- }
- function tokenize(stream) {
- this.query.lastIndex = stream.pos;
- const match = this.query.exec(stream.string);
- if (match && match.index === stream.pos) {
- this.numFound++;
- const t = performance.now();
- if (t - this.tallyShownTime > 10) {
- this.tallyShownTime = t;
- debounce(showTally);
- }
- stream.pos += match[0].length || 1;
- return MATCH_TOKEN_NAME;
- } else if (match) {
- stream.pos = match.index;
- } else {
- stream.skipToEnd();
- }
- }
- function annotateScrollbar(cm, query, icase) {
- getStateSafe(cm).annotate = cm.showMatchesOnScrollbar(query, icase, ANNOTATE_SCROLLBAR_OPTIONS);
- debounce(showTally);
- }
- //endregion
- //region Dialog
- function focusDialog(type, cm) {
- setActiveEditor(cm);
- const dialogFocused = state.dialog && state.dialog.contains(document.activeElement);
- let sel = dialogFocused ? '' : getSelection().toString() || cm && cm.getSelection();
- sel = !sel.includes('\n') && !sel.includes('\r') && sel;
- if (sel) state.find = sel;
- if (!dialogShown(type)) {
- destroyDialog();
- createDialog(type);
- } else if (sel) {
- setInputValue(state.input, sel);
- }
- state.input.focus();
- state.input.select();
- if (state.find) {
- doSearch({canAdvance: false});
- }
- }
- function dialogShown(type) {
- return document.body.contains(state.input) &&
- (!type || state.dialog.dataset.type === type);
- }
- function createDialog(type) {
- state.originalFocus = document.activeElement;
- state.firstRun = true;
- const dialog = state.dialog = t.template.searchReplaceDialog.cloneNode(true);
- Object.assign(dialog, DIALOG_PROPS.dialog);
- dialog.on('focusout', EVENTS.onfocusout);
- dialog.dataset.type = type;
- dialog.style.pointerEvents = 'auto';
- const content = $('[data-type="content"]', dialog);
- content.parentNode.replaceChild(t.template[type].cloneNode(true), content);
- createInput(0, 'input', state.find);
- createInput(1, 'input2', state.replace);
- toggleDataset($('[data-action="case"]', dialog), 'enabled', !state.icase);
- state.tally = $('[data-type="tally"]', dialog);
- const colors = {
- body: colorMimicry(document.body, {bg: 'backgroundColor'}),
- input: colorMimicry($('input:not(:disabled)'), {bg: 'backgroundColor'}),
- icon: colorMimicry($$('svg.info')[1], {fill: 'fill'}),
- };
- document.documentElement.appendChild(
- $(DIALOG_STYLE_SELECTOR) ||
- $create('style' + DIALOG_STYLE_SELECTOR)
- ).textContent = `
- #search-replace-dialog {
- background-color: ${colors.body.bg};
- }
- #search-replace-dialog textarea {
- color: ${colors.body.fore};
- background-color: ${colors.input.bg};
- }
- #search-replace-dialog svg {
- fill: ${colors.icon.fill};
- }
- #search-replace-dialog [data-action="case"] {
- color: ${colors.icon.fill};
- }
- #search-replace-dialog[data-type="replace"] button:hover svg,
- #search-replace-dialog svg:hover {
- fill: inherit;
- }
- #search-replace-dialog [data-action="case"]:hover {
- color: inherit;
- }
- #search-replace-dialog [data-action="clear"] {
- background-color: ${colors.input.bg.replace(/[^,]+$/, '') + '.75)'};
- }
- `;
- document.body.appendChild(dialog);
- dispatchEvent(new Event('showHotkeyInTooltip'));
- adjustTextareaSize(state.input);
- if (type === 'replace') {
- adjustTextareaSize(state.input2);
- enableReplaceButtons(state.find !== '');
- enableUndoButton(state.undoHistory.length);
- }
- return dialog;
- }
- function createInput(index, name, value) {
- const input = state[name] = $$('textarea', state.dialog)[index];
- if (!input) {
- return;
- }
- input.value = value;
- Object.assign(input, DIALOG_PROPS[name]);
- input.parentElement.appendChild(t.template.clearSearch.cloneNode(true));
- $('[data-action]', input.parentElement)._input = input;
- }
- function destroyDialog({restoreFocus = false} = {}) {
- state.input = null;
- $remove(DIALOG_SELECTOR);
- debounce.unregister(doSearch);
- makeTargetVisible(null);
- if (restoreFocus) {
- setTimeout(focusNoScroll, 0, state.originalFocus);
- } else {
- saveWindowScrollPos();
- restoreWindowScrollPos({immediately: false});
- }
- }
- function adjustTextareaSize(el) {
- const oldWidth = parseFloat(el.style.width) || el.clientWidth;
- const widthHistory = el._widthHistory = el._widthHistory || new Map();
- const knownWidth = widthHistory.get(el.value);
- let newWidth;
- if (knownWidth) {
- newWidth = knownWidth;
- } else {
- const hasVerticalScrollbar = el.scrollHeight > el.clientHeight;
- newWidth = el.scrollWidth + (hasVerticalScrollbar ? el.scrollWidth - el.clientWidth : 0);
- newWidth += newWidth > oldWidth ? 50 : 0;
- widthHistory.set(el.value, newWidth);
- }
- if (newWidth !== oldWidth) {
- const dialogRightOffset = parseFloat(getComputedStyle(state.dialog).right);
- const dialogRight = state.dialog.getBoundingClientRect().right;
- const textRight = (state.input2 || state.input).getBoundingClientRect().right;
- newWidth = Math.min(newWidth,
- (window.innerWidth - dialogRightOffset - (dialogRight - textRight)) / (state.input2 ? 2 : 1) - 20);
- el.style.width = newWidth + 'px';
- }
- const numLines = el.value.split('\n').length;
- if (numLines !== Number(el.rows)) {
- el.rows = numLines;
- }
- el.style.overflowX = el.scrollWidth > el.clientWidth ? '' : 'hidden';
- }
- function enableReplaceButtons(enabled) {
- if (state.dialog && state.dialog.dataset.type === 'replace') {
- for (const el of $$('[data-action^="replace"]', state.dialog)) {
- el.disabled = !enabled;
- }
- }
- }
- function enableUndoButton(enabled) {
- if (state.dialog && state.dialog.dataset.type === 'replace') {
- for (const el of $$('[data-action="undo"]', state.dialog)) {
- el.disabled = !enabled;
- }
- }
- }
- function focusUndoButton() {
- for (const btn of $$('[data-action="undo"]', state.dialog)) {
- if (getComputedStyle(btn).display !== 'none') {
- btn.focus();
- break;
- }
- }
- }
- //endregion
- //region Utility
- function getStateSafe(cm) {
- return cm.state.search || (cm.state.search = {});
- }
- // determines search start position:
- // the cursor if it was moved or the last match
- function getContinuationPos({cm, reverse}) {
- const cmSearchState = getStateSafe(cm);
- const posType = reverse ? 'from' : 'to';
- const searchPos = (cmSearchState.searchPos || {})[posType];
- const cursorPos = cm.getCursor(posType);
- const preferCursor = !searchPos || CodeMirror.cmpPos(cursorPos, cmSearchState.cursorPos[posType]);
- return preferCursor ? cursorPos : searchPos;
- }
- function getEOF(cm) {
- const line = cm.doc.size - 1;
- return {line, ch: cm.getLine(line).length};
- }
- function getNextEditor(cm, step = 1) {
- const editors = state.editors;
- return editors[(editors.indexOf(cm) + step + editors.length) % editors.length];
- }
- // sets the editor to start the search in
- // e.g. when the user switched to another editor and invoked a search command
- function setActiveEditor(cm) {
- if (cm.display.wrapper.contains(document.activeElement)) {
- state.cm = cm;
- state.originalFocus = cm;
- }
- }
- // adds a class on the editor that contains the active match
- // instead of focusing it (in order to keep the minidialog focused)
- function makeTargetVisible(element) {
- const old = $('.' + TARGET_CLASS);
- if (old !== element) {
- if (old) {
- old.classList.remove(TARGET_CLASS);
- document.body.classList.remove('find-open');
- }
- if (element) {
- element.classList.add(TARGET_CLASS);
- document.body.classList.add('find-open');
- }
- }
- }
- // scrolls the editor to reveal the match
- function makeMatchVisible(cm, searchCursor) {
- const canFocus = !state.firstRun && (!state.dialog || !state.dialog.contains(document.activeElement));
- state.cm = cm;
- // scroll within the editor
- const pos = searchCursor.pos;
- Object.assign(getStateSafe(cm), {
- cursorPos: {
- from: cm.getCursor('from'),
- to: cm.getCursor('to'),
- },
- searchPos: pos,
- unclosedOp: !cm.curOp,
- });
- if (!cm.curOp) cm.startOperation();
- if (!state.firstRun) {
- cm.jumpToPos(pos.from, pos.to);
- }
- // focus or expose as the current search target
- clearMarker();
- if (canFocus) {
- cm.focus();
- makeTargetVisible(null);
- } else {
- makeTargetVisible(cm.display.wrapper);
- // mark the match
- state.marker = cm.state.search.marker = cm.markText(pos.from, pos.to, {
- className: MATCH_CLASS,
- clearOnEnter: true,
- });
- }
- }
- function clearMarker() {
- if (state.marker) state.marker.clear();
- }
- function showTally(num, numApplies) {
- if (!state.tally) return;
- if (num === undefined) {
- num = 0;
- for (const cm of state.editors) {
- const {annotate, overlay} = getStateSafe(cm);
- num +=
- ((annotate || {}).matches || []).length ||
- (overlay || {}).numFound ||
- 0;
- }
- state.numFound = num;
- }
- if (numApplies === undefined && state.searchInApplies && state.numApplies < 0) {
- numApplies = 0;
- const elements = state.find ? document.getElementsByClassName(APPLIES_VALUE_CLASS) : [];
- const {rx} = state;
- for (const el of elements) {
- const value = el.value;
- if (rx) {
- rx.lastIndex = 0;
- // preventing an infinite loop if matched an empty string and didn't advance
- for (let m; (m = rx.exec(value)) && ++numApplies && rx.lastIndex > m.index;) { /* NOP */ }
- } else {
- let i = -1;
- while ((i = value.indexOf(state.find, i + 1)) >= 0) numApplies++;
- }
- }
- state.numApplies = numApplies;
- } else {
- numApplies = state.numApplies;
- }
- const newText = num + (numApplies > 0 ? '+' + numApplies : '');
- if (state.tally.textContent !== newText) {
- state.tally.textContent = newText;
- const newTitle = t('searchNumberOfResults' + (numApplies ? '2' : ''));
- if (state.tally.title !== newTitle) state.tally.title = newTitle;
- }
- }
- function trimUndoHistory() {
- const history = state.undoHistory;
- for (let last; (last = history[history.length - 1]);) {
- const undoables = last.filter(([cm, generation]) =>
- document.body.contains(cm.display.wrapper) && !cm.isClean(generation));
- if (undoables.length) {
- history[history.length - 1] = undoables;
- break;
- }
- history.length--;
- }
- }
- function focusNoScroll(el) {
- if (el) {
- saveWindowScrollPos();
- el.focus({preventScroll: true});
- restoreWindowScrollPos({immediately: false});
- }
- }
- function saveWindowScrollPos() {
- state.scrollX = window.scrollX;
- state.scrollY = window.scrollY;
- }
- function restoreWindowScrollPos({immediately = true} = {}) {
- if (!immediately) {
- // run in the next microtask cycle
- Promise.resolve().then(restoreWindowScrollPos);
- return;
- }
- if (window.scrollX !== state.scrollX || window.scrollY !== state.scrollY) {
- window.scrollTo(state.scrollX, state.scrollY);
- }
- }
- // produces [i, i+1, i-1, i+2, i-2, i+3, i-3, ...]
- function radiateArray(arr, focalIndex) {
- const focus = arr[focalIndex];
- if (!focus) return arr;
- const result = [focus];
- const len = arr.length;
- for (let i = 1; i < len; i++) {
- if (focalIndex + i < len) {
- result.push(arr[focalIndex + i]);
- }
- if (focalIndex - i >= 0) {
- result.push(arr[focalIndex - i]);
- }
- }
- return result;
- }
- function readStorage() {
- chromeLocal.getValue('editor').then((editor = {}) => {
- state.find = editor.find || '';
- state.replace = editor.replace || '';
- state.icase = editor.icase || state.icase;
- });
- }
- function writeStorage() {
- chromeLocal.getValue('editor').then((editor = {}) =>
- chromeLocal.setValue('editor', Object.assign(editor, {
- find: state.find,
- replace: state.replace,
- icase: state.icase,
- })));
- }
- function setInputValue(input, value) {
- input.focus();
- input.select();
- // using execCommand to add to the input's undo history
- document.execCommand(value ? 'insertText' : 'delete', false, value);
- // some versions of Firefox ignore execCommand
- if (input.value !== value) {
- input.value = value;
- input.dispatchEvent(new Event('input', {bubbles: true}));
- }
- }
- //endregion
- })();
|