editor: rewrite global search/replace
This commit is contained in:
parent
f29e3bc587
commit
d10e83d15c
|
@ -81,7 +81,7 @@ rules:
|
||||||
computed-property-spacing: [2, never]
|
computed-property-spacing: [2, never]
|
||||||
consistent-return: [0]
|
consistent-return: [0]
|
||||||
constructor-super: [2]
|
constructor-super: [2]
|
||||||
curly: [2]
|
curly: [2, "multi-line"]
|
||||||
default-case: [0]
|
default-case: [0]
|
||||||
dot-location: [2, property]
|
dot-location: [2, property]
|
||||||
dot-notation: [0]
|
dot-notation: [0]
|
||||||
|
@ -121,7 +121,7 @@ rules:
|
||||||
no-case-declarations: [2]
|
no-case-declarations: [2]
|
||||||
no-class-assign: [2]
|
no-class-assign: [2]
|
||||||
no-cond-assign: [2, except-parens]
|
no-cond-assign: [2, except-parens]
|
||||||
no-confusing-arrow: [1, {allowParens: true}]
|
no-confusing-arrow: [0, {allowParens: true}]
|
||||||
no-const-assign: [2]
|
no-const-assign: [2]
|
||||||
no-constant-condition: [0]
|
no-constant-condition: [0]
|
||||||
no-continue: [0]
|
no-continue: [0]
|
||||||
|
@ -219,7 +219,7 @@ rules:
|
||||||
no-unreachable: [2]
|
no-unreachable: [2]
|
||||||
no-unsafe-finally: [2]
|
no-unsafe-finally: [2]
|
||||||
no-unsafe-negation: [2]
|
no-unsafe-negation: [2]
|
||||||
no-unused-expressions: [1, {allowShortCircuit: true, allowTernary: true}]
|
no-unused-expressions: [1]
|
||||||
no-unused-labels: [0]
|
no-unused-labels: [0]
|
||||||
no-unused-vars: [1, {args: after-used, vars: local, argsIgnorePattern: ^_}]
|
no-unused-vars: [1, {args: after-used, vars: local, argsIgnorePattern: ^_}]
|
||||||
no-use-before-define: [2, nofunc]
|
no-use-before-define: [2, nofunc]
|
||||||
|
|
|
@ -214,6 +214,14 @@
|
||||||
"message": "History",
|
"message": "History",
|
||||||
"description": "Used in various places to show a history log of something"
|
"description": "Used in various places to show a history log of something"
|
||||||
},
|
},
|
||||||
|
"genericNext": {
|
||||||
|
"message": "Next",
|
||||||
|
"description": "Used in various places to select/perform the next step/action"
|
||||||
|
},
|
||||||
|
"genericPrevious": {
|
||||||
|
"message": "Previous",
|
||||||
|
"description": "Used in various places to select/perform the previous step/action"
|
||||||
|
},
|
||||||
"genericResetLabel": {
|
"genericResetLabel": {
|
||||||
"message": "Reset",
|
"message": "Reset",
|
||||||
"description": "Used in various parts of UI to indicate that something may be reset to its original state"
|
"description": "Used in various parts of UI to indicate that something may be reset to its original state"
|
||||||
|
@ -737,6 +745,18 @@
|
||||||
"message": "Search",
|
"message": "Search",
|
||||||
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
"description": "Label before the search input field in the editor shown on Ctrl-F"
|
||||||
},
|
},
|
||||||
|
"searchCaseSensitive": {
|
||||||
|
"message": "Case-sensitive",
|
||||||
|
"description": "Tooltip for the 'Aa' icon that enables case-sensitive search in the editor shown on Ctrl-F"
|
||||||
|
},
|
||||||
|
"searchNumberOfResults": {
|
||||||
|
"message": "Number of matches",
|
||||||
|
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
|
||||||
|
},
|
||||||
|
"searchNumberOfResults2": {
|
||||||
|
"message": "Number of matches in code and applies-to values",
|
||||||
|
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
|
||||||
|
},
|
||||||
"searchRegexp": {
|
"searchRegexp": {
|
||||||
"message": "Use /re/ syntax for regexp search",
|
"message": "Use /re/ syntax for regexp search",
|
||||||
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||||
|
|
136
edit.html
136
edit.html
|
@ -3,8 +3,8 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<link rel="stylesheet" href="global.css">
|
<link href="global.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="edit/edit.css">
|
<link href="edit/edit.css" rel="stylesheet">
|
||||||
|
|
||||||
<style id="firefox-transitions-bug-suppressor">
|
<style id="firefox-transitions-bug-suppressor">
|
||||||
/* restrict to FF */
|
/* restrict to FF */
|
||||||
|
@ -36,31 +36,33 @@
|
||||||
<script src="edit/codemirror-editing-hooks.js"></script>
|
<script src="edit/codemirror-editing-hooks.js"></script>
|
||||||
<script src="edit/edit.js"></script>
|
<script src="edit/edit.js"></script>
|
||||||
|
|
||||||
|
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
||||||
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
||||||
<link rel="stylesheet" href="vendor/codemirror/lib/codemirror.css">
|
|
||||||
<script src="vendor/codemirror/mode/css/css.js"></script>
|
<script src="vendor/codemirror/mode/css/css.js"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="vendor/codemirror/addon/dialog/dialog.css">
|
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="vendor/codemirror/addon/search/matchesonscrollbar.css">
|
|
||||||
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
|
||||||
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
|
||||||
<script src="vendor/codemirror/addon/search/match-highlighter.js"></script>
|
|
||||||
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
||||||
|
|
||||||
|
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
|
||||||
|
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||||
|
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||||
|
<script src="vendor/codemirror/addon/search/match-highlighter.js"></script>
|
||||||
<script src="vendor/codemirror/addon/search/searchcursor.js"></script>
|
<script src="vendor/codemirror/addon/search/searchcursor.js"></script>
|
||||||
<script src="vendor/codemirror/addon/search/search.js"></script>
|
|
||||||
<script src="vendor/codemirror/addon/comment/comment.js"></script>
|
<script src="vendor/codemirror/addon/comment/comment.js"></script>
|
||||||
<script src="vendor/codemirror/addon/selection/active-line.js"></script>
|
<script src="vendor/codemirror/addon/selection/active-line.js"></script>
|
||||||
|
<script src="vendor/codemirror/addon/edit/matchbrackets.js"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="vendor/codemirror/addon/fold/foldgutter.css" />
|
<link href="vendor/codemirror/addon/fold/foldgutter.css" rel="stylesheet" />
|
||||||
<script src="vendor/codemirror/addon/fold/foldcode.js"></script>
|
<script src="vendor/codemirror/addon/fold/foldcode.js"></script>
|
||||||
<script src="vendor/codemirror/addon/fold/foldgutter.js"></script>
|
<script src="vendor/codemirror/addon/fold/foldgutter.js"></script>
|
||||||
<script src="vendor/codemirror/addon/fold/brace-fold.js"></script>
|
<script src="vendor/codemirror/addon/fold/brace-fold.js"></script>
|
||||||
<script src="vendor/codemirror/addon/fold/comment-fold.js"></script>
|
<script src="vendor/codemirror/addon/fold/comment-fold.js"></script>
|
||||||
|
|
||||||
<script src="vendor/codemirror/addon/edit/matchbrackets.js"></script>
|
<link href="vendor/codemirror/addon/lint/lint.css" rel="stylesheet" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="vendor/codemirror/addon/lint/lint.css" />
|
<link href="vendor/codemirror/addon/hint/show-hint.css" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="vendor/codemirror/addon/hint/show-hint.css" />
|
|
||||||
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
||||||
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
|
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
|
||||||
|
|
||||||
|
@ -68,14 +70,18 @@
|
||||||
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
||||||
<script src="vendor/codemirror/keymap/vim.js"></script>
|
<script src="vendor/codemirror/keymap/vim.js"></script>
|
||||||
|
|
||||||
<link href="/vendor-overwrites/colorpicker/colorpicker.css" rel="stylesheet">
|
<link href="vendor-overwrites/colorpicker/colorpicker.css" rel="stylesheet">
|
||||||
<script src="/vendor-overwrites/colorpicker/colorpicker.js"></script>
|
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||||
<script src="/vendor-overwrites/colorpicker/colorview.js"></script>
|
<script src="vendor-overwrites/colorpicker/colorview.js"></script>
|
||||||
|
|
||||||
|
<link href="edit/global-search.css" rel="stylesheet">
|
||||||
|
<script src="edit/global-search.js"></script>
|
||||||
|
|
||||||
<script src="edit/match-highlighter-helper.js"></script>
|
<script src="edit/match-highlighter-helper.js"></script>
|
||||||
|
|
||||||
|
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||||
<script src="edit/codemirror-default.js"></script>
|
<script src="edit/codemirror-default.js"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/edit/codemirror-default.css">
|
|
||||||
<link id="cm-theme" rel="stylesheet">
|
<link id="cm-theme" rel="stylesheet">
|
||||||
|
|
||||||
<template data-id="appliesTo">
|
<template data-id="appliesTo">
|
||||||
|
@ -120,35 +126,75 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template data-id="searchReplaceDialog">
|
||||||
|
<div id="search-replace-dialog">
|
||||||
|
<div data-type="main">
|
||||||
|
<div data-type="content"></div>
|
||||||
|
<div data-type="actions">
|
||||||
|
<a data-action="case" i18n-title="searchCaseSensitive" href="#">Aa</a>
|
||||||
|
<a data-action="prev" i18n-title="genericPrevious" href="#" data-hotkey-tooltip="findPrev">
|
||||||
|
<svg class="svg-icon" style="transform: rotate(180deg)"><use xlink:href="#svg-icon-v"/></svg>
|
||||||
|
</a>
|
||||||
|
<a data-action="next" i18n-title="genericNext" href="#" data-hotkey-tooltip="findNext">
|
||||||
|
<svg class="svg-icon"><use xlink:href="#svg-icon-v"/></svg>
|
||||||
|
</a>
|
||||||
|
<a data-action="close" i18n-title="confirmClose" href="#" data-hotkey-tooltip="defocusEditor">
|
||||||
|
<svg class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-type="status">
|
||||||
|
<div class="CodeMirror-search-hint" i18n-text="searchRegexp"></div>
|
||||||
|
<div data-type="tally" i18n-title="searchNumberOfResults"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template data-id="clearSearch">
|
||||||
|
<div data-type="hover" i18n-title="confirmDelete">
|
||||||
|
<svg data-action="clear" class="svg-icon"><use xlink:href="#svg-icon-close"></use></svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template data-id="find">
|
<template data-id="find">
|
||||||
<span i18n-text="search">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
<div data-type="content">
|
||||||
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
<div data-type="input-wrapper">
|
||||||
</span>
|
<textarea class="CodeMirror-search-field" rows="1" spellcheck="false" required
|
||||||
|
i18n-placeholder="search"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="replace">
|
<template data-id="replace">
|
||||||
<span i18n-text="replace">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
<div data-type="content">
|
||||||
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
<div data-type="input-wrapper">
|
||||||
</span>
|
<textarea data-type="replace-from"
|
||||||
</template>
|
i18n-placeholder="replace"
|
||||||
|
class="CodeMirror-search-field" rows="1" required
|
||||||
<template data-id="replaceAll">
|
spellcheck="false"></textarea>
|
||||||
<span i18n-text="replaceAll">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
</div>
|
||||||
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
<div data-type="input-wrapper">
|
||||||
</span>
|
<textarea data-type="replace-to"
|
||||||
</template>
|
i18n-placeholder="replaceWith"
|
||||||
|
class="CodeMirror-search-field" rows="1" required
|
||||||
<template data-id="replaceWith">
|
spellcheck="false"></textarea>
|
||||||
<span i18n-text="replaceWith">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
</div>
|
||||||
</span>
|
<button data-action="replace" i18n-text="replace" disabled>
|
||||||
</template>
|
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||||
|
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
|
||||||
<template data-id="replaceConfirm">
|
</svg>
|
||||||
<span i18n-text="replace">?
|
</button>
|
||||||
<button i18n-text="confirmYes"></button>
|
<button data-action="replaceAll" i18n-text="replaceAll" disabled>
|
||||||
<button i18n-text="confirmNo"></button>
|
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||||
<button i18n-text="confirmStop"></button>
|
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
|
||||||
</span>
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button data-action="undo" i18n-text="undo" disabled>
|
||||||
|
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||||
|
<path d="M11.3,5.5H8.7V1.4L1.9,6.5l6.8,5.1V7.5h2.6c1.8,0,3.2,1.4,3.2,3.2s-1.4,3.2-3.2,3.2H7.8v2h3.5c2.9,0,5.2-2.3,5.2-5.2S14.2,5.5,11.3,5.5z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template data-id="jumpToLine">
|
<template data-id="jumpToLine">
|
||||||
|
@ -201,7 +247,7 @@
|
||||||
</section>
|
</section>
|
||||||
<section id="actions">
|
<section id="actions">
|
||||||
<div>
|
<div>
|
||||||
<button id="save-button" i18n-text="styleSaveLabel"></button>
|
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save"></button>
|
||||||
<button id="beautify" i18n-text="styleBeautify"></button>
|
<button id="beautify" i18n-text="styleBeautify"></button>
|
||||||
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -351,6 +397,10 @@
|
||||||
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
|
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="svg-icon-v" viewBox="0 0 16 16">
|
||||||
|
<path d="M8,11.5L2.8,6.3l1.5-1.5L8,8.6l3.7-3.7l1.5,1.5L8,11.5z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
<symbol id="svg-icon-settings" viewBox="0 0 16 16">
|
<symbol id="svg-icon-settings" viewBox="0 0 16 16">
|
||||||
<path d="M8,0C7.6,0,7.3,0,6.9,0.1v2.2C6.1,2.5,5.4,2.8,4.8,3.2L3.2,1.6c-0.6,0.4-1.1,1-1.6,1.6l1.6,1.6C2.8,5.4,2.5,6.1,2.3,6.9H0.1C0,7.3,0,7.6,0,8c0,0.4,0,0.7,0.1,1.1h2.2c0.1,0.8,0.4,1.5,0.9,2.1l-1.6,1.6c0.4,0.6,1,1.1,1.6,1.6l1.6-1.6c0.6,0.4,1.4,0.7,2.1,0.9v2.2C7.3,16,7.6,16,8,16c0.4,0,0.7,0,1.1-0.1v-2.2c0.8-0.1,1.5-0.4,2.1-0.9l1.6,1.6c0.6-0.4,1.1-1,1.6-1.6l-1.6-1.6c0.4-0.6,0.7-1.4,0.9-2.1h2.2C16,8.7,16,8.4,16,8c0-0.4,0-0.7-0.1-1.1h-2.2c-0.1-0.8-0.4-1.5-0.9-2.1l1.6-1.6c-0.4-0.6-1-1.1-1.6-1.6l-1.6,1.6c-0.6-0.4-1.4-0.7-2.1-0.9V0.1C8.7,0,8.4,0,8,0z M8,4.3c2.1,0,3.7,1.7,3.7,3.7c0,0,0,0,0,0c0,2.1-1.7,3.7-3.7,3.7c0,0,0,0,0,0c-2.1,0-3.7-1.7-3.7-3.7c0,0,0,0,0,0C4.3,5.9,5.9,4.3,8,4.3C8,4.3,8,4.3,8,4.3z"/>
|
<path d="M8,0C7.6,0,7.3,0,6.9,0.1v2.2C6.1,2.5,5.4,2.8,4.8,3.2L3.2,1.6c-0.6,0.4-1.1,1-1.6,1.6l1.6,1.6C2.8,5.4,2.5,6.1,2.3,6.9H0.1C0,7.3,0,7.6,0,8c0,0.4,0,0.7,0.1,1.1h2.2c0.1,0.8,0.4,1.5,0.9,2.1l-1.6,1.6c0.4,0.6,1,1.1,1.6,1.6l1.6-1.6c0.6,0.4,1.4,0.7,2.1,0.9v2.2C7.3,16,7.6,16,8,16c0.4,0,0.7,0,1.1-0.1v-2.2c0.8-0.1,1.5-0.4,2.1-0.9l1.6,1.6c0.6-0.4,1.1-1,1.6-1.6l-1.6-1.6c0.4-0.6,0.7-1.4,0.9-2.1h2.2C16,8.7,16,8.4,16,8c0-0.4,0-0.7-0.1-1.1h-2.2c-0.1-0.8-0.4-1.5-0.9-2.1l1.6-1.6c-0.4-0.6-1-1.1-1.6-1.6l-1.6,1.6c-0.6-0.4-1.4-0.7-2.1-0.9V0.1C8.7,0,8.4,0,8,0z M8,4.3c2.1,0,3.7,1.7,3.7,3.7c0,0,0,0,0,0c0,2.1-1.7,3.7-3.7,3.7c0,0,0,0,0,0c-2.1,0-3.7-1.7-3.7-3.7c0,0,0,0,0,0C4.3,5.9,5.9,4.3,8,4.3C8,4.3,8,4.3,8,4.3z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
|
@ -15,35 +15,32 @@ onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
jumpToLine,
|
jumpToLine,
|
||||||
defocusEditor,
|
defocusEditor,
|
||||||
nextEditor, prevEditor,
|
nextEditor, prevEditor,
|
||||||
find, findNext, findPrev, replace, replaceAll,
|
|
||||||
};
|
};
|
||||||
// reroute handling to nearest editor when keypress resolves to one of these commands
|
// reroute handling to nearest editor when keypress resolves to one of these commands
|
||||||
const REROUTED = new Set([
|
const REROUTED = new Set([
|
||||||
...Object.keys(COMMANDS),
|
...Object.keys(COMMANDS),
|
||||||
|
'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
|
||||||
'colorpicker',
|
'colorpicker',
|
||||||
]);
|
]);
|
||||||
const ORIGINAL_COMMAND = {};
|
|
||||||
const ORIGINAL_METHOD = {};
|
|
||||||
Object.assign(CodeMirror, {
|
Object.assign(CodeMirror, {
|
||||||
getOption,
|
getOption,
|
||||||
setOption,
|
setOption,
|
||||||
|
closestVisible,
|
||||||
});
|
});
|
||||||
Object.assign(CodeMirror.prototype, {
|
Object.assign(CodeMirror.prototype, {
|
||||||
getSection,
|
getSection,
|
||||||
rerouteHotkeys,
|
rerouteHotkeys,
|
||||||
});
|
});
|
||||||
|
|
||||||
// cm.state.search for last used 'find'
|
|
||||||
let searchState;
|
|
||||||
|
|
||||||
new MutationObserver((mutations, observer) => {
|
new MutationObserver((mutations, observer) => {
|
||||||
if (!$('#sections')) {
|
if (!$('#sections')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
|
||||||
prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
|
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
||||||
showKeyInSaveButtonTooltip();
|
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
||||||
|
showHotkeyInTooltip();
|
||||||
|
|
||||||
// N.B. the event listener should be registered before setupLivePrefs()
|
// N.B. the event listener should be registered before setupLivePrefs()
|
||||||
$('#options').addEventListener('change', onOptionElementChanged);
|
$('#options').addEventListener('change', onOptionElementChanged);
|
||||||
|
@ -51,8 +48,8 @@ onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
buildKeymapElement();
|
buildKeymapElement();
|
||||||
setupLivePrefs();
|
setupLivePrefs();
|
||||||
|
|
||||||
|
Object.assign(CodeMirror.commands, COMMANDS);
|
||||||
rerouteHotkeys(true);
|
rerouteHotkeys(true);
|
||||||
setupFindHooks();
|
|
||||||
}).observe(document, {childList: true, subtree: true});
|
}).observe(document, {childList: true, subtree: true});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -269,7 +266,7 @@ onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
const themeElement = $('#editor.theme');
|
const themeElement = $('#editor.theme');
|
||||||
const themeList = localStorage.codeMirrorThemes;
|
const themeList = localStorage.codeMirrorThemes;
|
||||||
|
|
||||||
const optionsFromArray = (options) => {
|
const optionsFromArray = options => {
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
options.forEach(opt => fragment.appendChild($create('option', opt)));
|
options.forEach(opt => fragment.appendChild($create('option', opt)));
|
||||||
themeElement.appendChild(fragment);
|
themeElement.appendChild(fragment);
|
||||||
|
@ -316,257 +313,11 @@ onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
el.dataset.default = '';
|
el.dataset.default = '';
|
||||||
el.title = t('defaultTheme');
|
el.title = t('defaultTheme');
|
||||||
}
|
}
|
||||||
!groupWithNext && (bin = fragment);
|
if (!groupWithNext) bin = fragment;
|
||||||
});
|
});
|
||||||
$('#editor.keyMap').appendChild(fragment);
|
$('#editor.keyMap').appendChild(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////
|
|
||||||
|
|
||||||
function setupFindHooks() {
|
|
||||||
for (const name of ['find', 'findNext', 'findPrev', 'replace']) {
|
|
||||||
ORIGINAL_COMMAND[name] = CodeMirror.commands[name];
|
|
||||||
}
|
|
||||||
for (const name of ['openDialog', 'openConfirm']) {
|
|
||||||
ORIGINAL_METHOD[name] = CodeMirror.prototype[name];
|
|
||||||
}
|
|
||||||
Object.assign(CodeMirror.commands, COMMANDS);
|
|
||||||
chrome.storage.local.get('editSearchText', data => {
|
|
||||||
searchState = {query: data.editSearchText || null};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIgnoreCase(query) {
|
|
||||||
// treat all-lowercase non-regexp queries as case-insensitive
|
|
||||||
return typeof query === 'string' && query === query.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateState(cm, newState) {
|
|
||||||
if (!newState) {
|
|
||||||
if ((cm.state.search || {}).overlay) {
|
|
||||||
return cm.state.search;
|
|
||||||
}
|
|
||||||
if (!searchState.overlay) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
newState = searchState;
|
|
||||||
}
|
|
||||||
cm.state.search = {
|
|
||||||
query: newState.query,
|
|
||||||
overlay: newState.overlay,
|
|
||||||
annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query))
|
|
||||||
};
|
|
||||||
cm.addOverlay(newState.overlay);
|
|
||||||
return cm.state.search;
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrides the original openDialog with a clone of the provided template
|
|
||||||
function customizeOpenDialog(cm, template, callback) {
|
|
||||||
cm.openDialog = (tmpl, cb, opt) => {
|
|
||||||
// invoke 'callback' and bind 'this' to the original callback
|
|
||||||
ORIGINAL_METHOD.openDialog.call(cm, template.cloneNode(true), callback.bind(cb), opt);
|
|
||||||
};
|
|
||||||
setTimeout(() => (cm.openDialog = ORIGINAL_METHOD.openDialog));
|
|
||||||
refocusMinidialog(cm);
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusClosestCM(activeCM) {
|
|
||||||
editors.lastActive = activeCM;
|
|
||||||
const cm = getEditorInSight();
|
|
||||||
if (cm !== activeCM) {
|
|
||||||
cm.focus();
|
|
||||||
}
|
|
||||||
return cm;
|
|
||||||
}
|
|
||||||
|
|
||||||
function propagateSearchState(cm) {
|
|
||||||
if ((cm.state.search || {}).clearSearch) {
|
|
||||||
cm.execCommand('clearSearch');
|
|
||||||
}
|
|
||||||
updateState(cm);
|
|
||||||
}
|
|
||||||
|
|
||||||
function find(activeCM) {
|
|
||||||
activeCM = focusClosestCM(activeCM);
|
|
||||||
const state = activeCM.state;
|
|
||||||
if (searchState.query && !(state.search || {}).lastQuery) {
|
|
||||||
(state.search = state.search || {}).query = searchState.query;
|
|
||||||
}
|
|
||||||
customizeOpenDialog(activeCM, template.find, function (query) {
|
|
||||||
this(query);
|
|
||||||
searchState = state.search;
|
|
||||||
if (searchState.query) {
|
|
||||||
chrome.storage.local.set({editSearchText: searchState.query});
|
|
||||||
}
|
|
||||||
if (!searchState.query ||
|
|
||||||
editors.length === 1 ||
|
|
||||||
CodeMirror.cmpPos(searchState.posFrom, searchState.posTo)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editors.forEach(cm => ((cm.state.search || {}).clearSearch = cm !== activeCM));
|
|
||||||
editors.forEach((cm, i) => setTimeout(propagateSearchState, i + 100, cm));
|
|
||||||
findNext(activeCM);
|
|
||||||
});
|
|
||||||
ORIGINAL_COMMAND.find(activeCM);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findNext(activeCM, reverse) {
|
|
||||||
let state = updateState(activeCM);
|
|
||||||
if (!state || !state.overlay) {
|
|
||||||
find(activeCM);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let pos = activeCM.getCursor(reverse ? 'from' : 'to');
|
|
||||||
// clear the selection, don't move the cursor
|
|
||||||
if (activeCM.somethingSelected()) {
|
|
||||||
activeCM.setSelection(activeCM.getCursor());
|
|
||||||
}
|
|
||||||
|
|
||||||
const icase = shouldIgnoreCase(state.query);
|
|
||||||
const query = searchState.query;
|
|
||||||
const rxQuery = typeof query === 'object'
|
|
||||||
? query : stringAsRegExp(query, icase ? 'i' : '');
|
|
||||||
|
|
||||||
const total = editors.length;
|
|
||||||
if ((!reverse || total === 1 ||
|
|
||||||
(document.activeElement || {}).name === 'applies-value') &&
|
|
||||||
findAppliesTo(activeCM, reverse, rxQuery)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let cm = activeCM;
|
|
||||||
const startIndex = editors.indexOf(cm);
|
|
||||||
for (let i = 1; i < total; i++) {
|
|
||||||
cm = editors[(startIndex + i * (reverse ? -1 : 1) + total) % total];
|
|
||||||
pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0);
|
|
||||||
const searchCursor = cm.getSearchCursor(query, pos, icase);
|
|
||||||
if (searchCursor.find(reverse)) {
|
|
||||||
if (total > 1) {
|
|
||||||
makeSectionVisible(cm);
|
|
||||||
cm.focus();
|
|
||||||
}
|
|
||||||
if ((cm.state.search || {}).clearSearch) {
|
|
||||||
cm.execCommand('clearSearch');
|
|
||||||
}
|
|
||||||
state = updateState(cm);
|
|
||||||
// speedup the original findNext
|
|
||||||
state.posFrom = reverse ? searchCursor.to() : searchCursor.from();
|
|
||||||
state.posTo = Object.assign({}, state.posFrom);
|
|
||||||
setTimeout(ORIGINAL_COMMAND[reverse ? 'findPrev' : 'findNext'], 0, cm);
|
|
||||||
return;
|
|
||||||
} else if (!reverse && findAppliesTo(cm, reverse, rxQuery)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cm = editors[(startIndex + (i + 1) * (reverse ? -1 : 1) + total) % total];
|
|
||||||
if (reverse && findAppliesTo(cm, reverse, rxQuery)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// nothing found so far, so call the original search with wrap-around
|
|
||||||
ORIGINAL_COMMAND[reverse ? 'findPrev' : 'findNext'](activeCM);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findAppliesTo(cm, reverse, rxQuery) {
|
|
||||||
let inputs = $$('.applies-value', cm.getSection());
|
|
||||||
if (reverse) {
|
|
||||||
inputs = inputs.reverse();
|
|
||||||
}
|
|
||||||
inputs.splice(0, inputs.indexOf(document.activeElement) + 1);
|
|
||||||
return inputs.some(input => {
|
|
||||||
const match = rxQuery.exec(input.value);
|
|
||||||
if (match) {
|
|
||||||
input.focus();
|
|
||||||
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);
|
|
||||||
}, 0);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function findPrev(cm) {
|
|
||||||
findNext(cm, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function replace(activeCM, all) {
|
|
||||||
let queue;
|
|
||||||
let query;
|
|
||||||
let replacement;
|
|
||||||
activeCM = focusClosestCM(activeCM);
|
|
||||||
customizeOpenDialog(activeCM, template[all ? 'replaceAll' : 'replace'], function (txt) {
|
|
||||||
query = txt;
|
|
||||||
customizeOpenDialog(activeCM, template.replaceWith, txt => {
|
|
||||||
replacement = txt;
|
|
||||||
queue = editors.rotate(-editors.indexOf(activeCM));
|
|
||||||
if (all) {
|
|
||||||
editors.forEach(doReplace);
|
|
||||||
} else {
|
|
||||||
doReplace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this(query);
|
|
||||||
});
|
|
||||||
ORIGINAL_COMMAND.replace(activeCM, all);
|
|
||||||
|
|
||||||
function doReplace() {
|
|
||||||
const cm = queue.shift();
|
|
||||||
if (!cm) {
|
|
||||||
if (!all) {
|
|
||||||
editors.lastActive.focus();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// hide the first two dialogs (replace, replaceWith)
|
|
||||||
cm.openDialog = (tmpl, callback) => {
|
|
||||||
cm.openDialog = (tmpl, callback) => {
|
|
||||||
cm.openDialog = ORIGINAL_METHOD.openDialog;
|
|
||||||
if (all) {
|
|
||||||
callback(replacement);
|
|
||||||
} else {
|
|
||||||
doConfirm(cm);
|
|
||||||
callback(replacement);
|
|
||||||
if (!$('.CodeMirror-dialog', cm.getWrapperElement())) {
|
|
||||||
// no dialog == nothing found in the current CM, move to the next
|
|
||||||
doReplace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
callback(query);
|
|
||||||
};
|
|
||||||
ORIGINAL_COMMAND.replace(cm, all);
|
|
||||||
}
|
|
||||||
function doConfirm(cm) {
|
|
||||||
let wrapAround = false;
|
|
||||||
const origPos = cm.getCursor();
|
|
||||||
cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) {
|
|
||||||
const ovrCallbacks = callbacks.map(callback => () => {
|
|
||||||
makeSectionVisible(cm);
|
|
||||||
cm.openConfirm = overrideConfirm;
|
|
||||||
setTimeout(() => (cm.openConfirm = ORIGINAL_METHOD.openConfirm));
|
|
||||||
|
|
||||||
const pos = cm.getCursor();
|
|
||||||
callback();
|
|
||||||
const cmp = CodeMirror.cmpPos(cm.getCursor(), pos);
|
|
||||||
wrapAround |= cmp <= 0;
|
|
||||||
|
|
||||||
const dlg = $('.CodeMirror-dialog', cm.getWrapperElement());
|
|
||||||
if (!dlg || cmp === 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) {
|
|
||||||
$.remove(dlg);
|
|
||||||
doReplace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ORIGINAL_METHOD.openConfirm.call(cm, template.replaceConfirm.cloneNode(true), ovrCallbacks, opt);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceAll(cm) {
|
|
||||||
replace(cm, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
////////////////////////////////////////////////
|
||||||
|
|
||||||
function rerouteHotkeys(enable, immediately) {
|
function rerouteHotkeys(enable, immediately) {
|
||||||
|
@ -586,7 +337,7 @@ onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
}
|
}
|
||||||
const rerouteCommand = name => {
|
const rerouteCommand = name => {
|
||||||
if (REROUTED.has(name)) {
|
if (REROUTED.has(name)) {
|
||||||
CodeMirror.commands[name](getEditorInSight(event.target));
|
CodeMirror.commands[name](closestVisible(event.target));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -599,13 +350,22 @@ onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
////////////////////////////////////////////////
|
||||||
|
|
||||||
function getEditorInSight(nearbyElement) {
|
// priority:
|
||||||
// priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible
|
// 1. associated CM for applies-to element
|
||||||
let cm;
|
// 2. last active if visible
|
||||||
if (nearbyElement && nearbyElement.className.indexOf('applies-') >= 0) {
|
// 3. first visible
|
||||||
cm = getSectionForChild(nearbyElement).CodeMirror;
|
function closestVisible(nearbyElement) {
|
||||||
} else {
|
const cm =
|
||||||
cm = editors.lastActive;
|
nearbyElement instanceof CodeMirror ? nearbyElement :
|
||||||
|
nearbyElement instanceof Node && (getSectionForChild(nearbyElement) || {}).CodeMirror ||
|
||||||
|
editors.lastActive;
|
||||||
|
if (nearbyElement instanceof Node && cm) {
|
||||||
|
const {left, top} = nearbyElement.getBoundingClientRect();
|
||||||
|
const bounds = cm.display.wrapper.getBoundingClientRect();
|
||||||
|
if (top >= 0 && top >= bounds.top &&
|
||||||
|
left >= 0 && left >= bounds.left) {
|
||||||
|
return cm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// closest editor should have at least 2 lines visible
|
// closest editor should have at least 2 lines visible
|
||||||
const lineHeight = editors[0].defaultTextHeight();
|
const lineHeight = editors[0].defaultTextHeight();
|
||||||
|
@ -621,6 +381,9 @@ onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
return distances[index];
|
return distances[index];
|
||||||
}
|
}
|
||||||
const section = (cm || editors[index]).getSection();
|
const section = (cm || editors[index]).getSection();
|
||||||
|
if (!section) {
|
||||||
|
return 1e9;
|
||||||
|
}
|
||||||
const top = allSectionsContainerTop + section.offsetTop;
|
const top = allSectionsContainerTop + section.offsetTop;
|
||||||
if (top < scrollY + lineHeight) {
|
if (top < scrollY + lineHeight) {
|
||||||
return Math.max(0, scrollY - top - lineHeight);
|
return Math.max(0, scrollY - top - lineHeight);
|
||||||
|
@ -743,12 +506,23 @@ onDOMscriptReady('/codemirror.js').then(() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showKeyInSaveButtonTooltip(prefName, value) {
|
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
|
||||||
$('#save-button').title = findKeyForCommand('save', value);
|
const extraKeys = CodeMirror.defaults.extraKeys;
|
||||||
|
for (const el of $$('[data-hotkey-tooltip]')) {
|
||||||
|
if (el._hotkeyTooltipKeyMap !== mapName) {
|
||||||
|
el._hotkeyTooltipKeyMap = mapName;
|
||||||
|
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
|
||||||
|
const cmd = el.dataset.hotkeyTooltip;
|
||||||
|
const key = findKeyForCommand(cmd, mapName) ||
|
||||||
|
extraKeys && findKeyForCommand(cmd, extraKeys);
|
||||||
|
const newTitle = title + (title && key ? '\n' : '') + (key || '');
|
||||||
|
if (el.title !== newTitle) el.title = newTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findKeyForCommand(command, mapName = CodeMirror.defaults.keyMap) {
|
function findKeyForCommand(command, map) {
|
||||||
const map = CodeMirror.keyMap[mapName];
|
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
||||||
let key = Object.keys(map).find(k => map[k] === command);
|
let key = Object.keys(map).find(k => map[k] === command);
|
||||||
if (key) {
|
if (key) {
|
||||||
return key;
|
return key;
|
||||||
|
|
232
edit/global-search.css
Normal file
232
edit/global-search.css
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
#search-replace-dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 64px;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: calc(100vw - 4rem);
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 10000;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 4px 5px 20px -6px rgba(0, 0, 0, .5);
|
||||||
|
border: 1px solid hsla(0, 0%, 50%, .4);
|
||||||
|
transition: opacity .25s;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog:not(:focus-within):not(:hover) {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog > * {
|
||||||
|
padding-right: .75em;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
background-color: hsla(0, 0%, 50%, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-type="content"] {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-type="content"] > :not(:last-child) {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-type="content"] button {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog[data-type="replace"] button svg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-type="input-wrapper"] {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog textarea {
|
||||||
|
resize: none;
|
||||||
|
width: min-content;
|
||||||
|
min-width: 10em;
|
||||||
|
max-width: none;
|
||||||
|
min-height: 1.3em;
|
||||||
|
max-height: 10vh;
|
||||||
|
line-height: 1.3em;
|
||||||
|
padding: .25rem .25rem .25rem .5rem;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: currentColor; /* use the current theme's color instead of UserAgent's CSS */
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog textarea:invalid {
|
||||||
|
box-shadow: none; /* Firefox is weird */
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog :disabled {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********** actions ****************/
|
||||||
|
|
||||||
|
#search-replace-dialog [data-type="actions"] {
|
||||||
|
flex: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: .5em;
|
||||||
|
opacity: .5;
|
||||||
|
transition: opacity .25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog:focus-within [data-type="actions"],
|
||||||
|
#search-replace-dialog [data-type="actions"]:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-action] {
|
||||||
|
display: block;
|
||||||
|
padding: 2px .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-type="actions"] a:last-child {
|
||||||
|
margin-right: -.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********** case-sensitivity ****************/
|
||||||
|
|
||||||
|
#search-replace-dialog [data-action="case"] {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: currentColor;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 .5em;
|
||||||
|
line-height: 20px;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-action="case"][data-enabled]:after {
|
||||||
|
content: "";
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
border-color: hsla(180, 100%, 30%, .5);
|
||||||
|
border-style: none none solid none;
|
||||||
|
border-width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-action="case"]:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********** clear input ****************/
|
||||||
|
|
||||||
|
#search-replace-dialog textarea:not(:valid) + [data-type="hover"] {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-type="hover"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 4px;
|
||||||
|
bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog:hover [data-type="hover"] {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-action="clear"] {
|
||||||
|
padding: 3px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: hsla(0, 0%, 100%, .75);
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-type="status"] {
|
||||||
|
background-color: hsla(0, 0%, 50%, .2);
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-left: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-type="tally"] {
|
||||||
|
opacity: .5;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-replace-dialog [data-type="tally"]:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********** CodeMirror ****************/
|
||||||
|
|
||||||
|
.search-target-editor {
|
||||||
|
outline: 1px solid darkorange;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stylus .search-target-match {
|
||||||
|
background-color: darkorange;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
#search-replace-dialog {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
#search-replace-dialog textarea {
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
#search-replace-dialog[data-type="replace"] {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
#search-replace-dialog[data-type="replace"] button {
|
||||||
|
font-size: 0;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#search-replace-dialog[data-type="replace"] button svg {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
#search-replace-dialog[data-type="replace"] textarea {
|
||||||
|
min-width: 50px;
|
||||||
|
max-width: calc(50vw - 120px);
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
#search-replace-dialog[data-type="replace"] button[data-action="replaceAll"] svg {
|
||||||
|
top: -3px;
|
||||||
|
position: relative;
|
||||||
|
/* unprefixed since Chrome 53 */
|
||||||
|
-webkit-filter: drop-shadow(0 5px 0 currentColor);
|
||||||
|
filter: drop-shadow(0 5px 0 currentColor);
|
||||||
|
}
|
||||||
|
}
|
895
edit/global-search.js
Normal file
895
edit/global-search.js
Normal file
|
@ -0,0 +1,895 @@
|
||||||
|
/* global CodeMirror editors makeSectionVisible */
|
||||||
|
/* global focusAccessibility */
|
||||||
|
/* global colorMimicry */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
onDOMready().then(() => {
|
||||||
|
|
||||||
|
//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 SCROLL_REVEAL_MIN_PX = 50;
|
||||||
|
|
||||||
|
const DIALOG_SELECTOR = '#search-replace-dialog';
|
||||||
|
const TARGET_CLASS = 'search-target-editor';
|
||||||
|
const MATCH_CLASS = 'search-target-match';
|
||||||
|
const MATCH_TOKEN_NAME = 'searching';
|
||||||
|
const OWN_STYLE_SELECTOR = '#global-search-style';
|
||||||
|
const APPLIES_VALUE_CLASS = 'applies-value';
|
||||||
|
|
||||||
|
const RX_MAYBE_REGEXP = /^\s*\/(.+?)\/([simguy]*)\s*$/;
|
||||||
|
|
||||||
|
const NARROW_WIDTH = [...document.styleSheets]
|
||||||
|
.filter(({href}) => href && href.endsWith('global-search.css'))
|
||||||
|
.map(sheet =>
|
||||||
|
[...sheet.cssRules]
|
||||||
|
.filter(r => r.media && r.conditionText.includes('max-width'))
|
||||||
|
.map(r => r.conditionText.match(/\d+/) | 0)
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
.pop())
|
||||||
|
.pop() || 800;
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
// 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,
|
||||||
|
|
||||||
|
undo: null,
|
||||||
|
undoHistory: [],
|
||||||
|
|
||||||
|
searchInApplies: !document.documentElement.classList.contains('usercss'),
|
||||||
|
};
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
//region Events
|
||||||
|
|
||||||
|
const ACTIONS = {
|
||||||
|
key: {
|
||||||
|
'Enter': event => {
|
||||||
|
if (event.target.closest(focusAccessibility.ELEMENTS.join(','))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
destroyDialog();
|
||||||
|
doSearch({canAdvance: false});
|
||||||
|
},
|
||||||
|
'Esc': () => {
|
||||||
|
destroyDialog({restoreFocus: true});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
click: {
|
||||||
|
next: () => doSearch({reverse: false}),
|
||||||
|
prev: () => doSearch({reverse: true}),
|
||||||
|
close: () => destroyDialog({restoreFocus: true}),
|
||||||
|
replace: () => doReplace(),
|
||||||
|
replaceAll: () => doReplaceAll(),
|
||||||
|
undo: () => doUndo(),
|
||||||
|
clear() {
|
||||||
|
this._input.focus();
|
||||||
|
document.execCommand('selectAll', false, null);
|
||||||
|
document.execCommand('delete', false, null);
|
||||||
|
},
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
return;
|
||||||
|
|
||||||
|
//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);
|
||||||
|
}
|
||||||
|
state.activeAppliesTo = $(`.${APPLIES_VALUE_CLASS}:focus, .${APPLIES_VALUE_CLASS}.${TARGET_CLASS}`);
|
||||||
|
state.cmStart = CodeMirror.closestVisible(
|
||||||
|
document.activeElement && document.activeElement.closest('.CodeMirror') && document.activeElement ||
|
||||||
|
state.activeAppliesTo || state.cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
if (!state.find) {
|
||||||
|
clearMarker();
|
||||||
|
makeTargetVisible(null);
|
||||||
|
setupOverlay(editors.slice());
|
||||||
|
showTally(0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {cmStart} = state;
|
||||||
|
const {index, found, foundInCode} = doSearchInEditors({cmStart, canAdvance, inApplies}) || {};
|
||||||
|
if (!foundInCode) clearMarker();
|
||||||
|
if (!found) makeTargetVisible(null);
|
||||||
|
const radiateFrom = foundInCode ? index : editors.indexOf(cmStart);
|
||||||
|
setupOverlay(radiateArray(editors, radiateFrom));
|
||||||
|
enableReplaceButtons(foundInCode);
|
||||||
|
debounce(showTally, 0, found && !state.numFound ? 1 : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 = editors.indexOf(cmStart);
|
||||||
|
const total = 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 = 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 : editors[index ? index - 1 : total - 1];
|
||||||
|
if (inApplies && doSearchInApplies(cmForNextApplies)) {
|
||||||
|
return {found: true};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function doSearchInApplies(cm, canAdvance) {
|
||||||
|
if (!state.searchInApplies) return;
|
||||||
|
const inputs = [...cm.getSection().getElementsByClassName(APPLIES_VALUE_CLASS)];
|
||||||
|
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);
|
||||||
|
makeSectionVisible(cm);
|
||||||
|
if (canFocus) input.focus();
|
||||||
|
state.cm = cm;
|
||||||
|
clearMarker();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
//region Replace
|
||||||
|
|
||||||
|
function doReplace() {
|
||||||
|
initState({initReplace: true});
|
||||||
|
const cm = state.cmStart;
|
||||||
|
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]);
|
||||||
|
state.undo.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function doReplaceAll() {
|
||||||
|
initState({initReplace: true});
|
||||||
|
clearMarker();
|
||||||
|
const found = editors.filter(cm => doReplaceInEditor({cm, all: true}));
|
||||||
|
if (found.length) {
|
||||||
|
state.lastFind = null;
|
||||||
|
state.undoHistory.push(found);
|
||||||
|
state.undo.disabled = false;
|
||||||
|
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 of state.undoHistory.pop() || []) {
|
||||||
|
if (document.body.contains(cm.display.wrapper) && !cm.isClean()) {
|
||||||
|
cm.undo();
|
||||||
|
cm.getAllMarks().forEach(marker =>
|
||||||
|
marker !== state.marker &&
|
||||||
|
marker.className === MATCH_CLASS &&
|
||||||
|
marker.clear());
|
||||||
|
undoneSome = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.undo.disabled = !state.undoHistory.length;
|
||||||
|
(state.undo.disabled ? state.input : state.undo).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) {
|
||||||
|
if (cmState.annotateTimer) clearTimeout(cmState.annotateTimer);
|
||||||
|
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++;
|
||||||
|
//state.numFound++;
|
||||||
|
const t = performance.now();
|
||||||
|
if (t - this.tallyShownTime > 10) {
|
||||||
|
debounce(showTally);
|
||||||
|
this.tallyShownTime = t;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
sel = !sel.includes('\n') && !sel.includes('\r') && sel;
|
||||||
|
if (sel) state.find = sel;
|
||||||
|
|
||||||
|
if (!dialogShown(type)) {
|
||||||
|
destroyDialog();
|
||||||
|
createDialog(type);
|
||||||
|
} else if (sel) {
|
||||||
|
state.input.focus();
|
||||||
|
state.input.select();
|
||||||
|
document.execCommand('insertText', false, 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;
|
||||||
|
|
||||||
|
const dialog = state.dialog = template.searchReplaceDialog.cloneNode(true);
|
||||||
|
Object.assign(dialog, DIALOG_PROPS.dialog);
|
||||||
|
dialog.dataset.type = type;
|
||||||
|
|
||||||
|
const content = $('[data-type="content"]', dialog);
|
||||||
|
content.parentNode.replaceChild(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.get(document.body, {bg: 'backgroundColor'}),
|
||||||
|
input: colorMimicry.get($('input:not(:disabled)'), {bg: 'backgroundColor'}),
|
||||||
|
icon: colorMimicry.get($$('svg.info')[1], {fill: 'fill'}),
|
||||||
|
};
|
||||||
|
document.documentElement.appendChild(
|
||||||
|
$(OWN_STYLE_SELECTOR) ||
|
||||||
|
$create('style' + OWN_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 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'));
|
||||||
|
|
||||||
|
measureInput(state.input);
|
||||||
|
adjustTextareaSize(state.input);
|
||||||
|
if (type === 'replace') {
|
||||||
|
measureInput(state.input2);
|
||||||
|
adjustTextareaSize(state.input2);
|
||||||
|
enableReplaceButtons(state.find !== '');
|
||||||
|
|
||||||
|
addEventListener('resize', toggleReplaceButtonTooltips, {passive: true});
|
||||||
|
toggleReplaceButtonTooltips(true);
|
||||||
|
|
||||||
|
state.undo = $('[data-action="undo"]');
|
||||||
|
state.undo.disabled = !state.undoHistory.length;
|
||||||
|
} else {
|
||||||
|
removeEventListener('resize', toggleReplaceButtonTooltips, {passive: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(template.clearSearch.cloneNode(true));
|
||||||
|
$('[data-action]', input.parentElement)._input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function measureInput(input) {
|
||||||
|
const style = getComputedStyle(input);
|
||||||
|
input._padding = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
|
||||||
|
input._maxWidth = parseFloat(style.maxWidth);
|
||||||
|
input._rowHeight = input.clientHeight - input._padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function destroyDialog({restoreFocus = false} = {}) {
|
||||||
|
state.input = null;
|
||||||
|
$.remove(DIALOG_SELECTOR);
|
||||||
|
debounce.unregister(doSearch);
|
||||||
|
makeTargetVisible(null);
|
||||||
|
removeEventListener('resize', toggleReplaceButtonTooltips, {passive: true});
|
||||||
|
if (restoreFocus) setTimeout(focusNoScroll, 0, state.originalFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 !== parseInt(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 toggleReplaceButtonTooltips(debounced) {
|
||||||
|
if (debounced !== true) {
|
||||||
|
debounce(toggleReplaceButtonTooltips, 0, true);
|
||||||
|
} else {
|
||||||
|
const addTitle = window.innerWidth <= NARROW_WIDTH;
|
||||||
|
for (const el of state.dialog.getElementsByTagName('button')) {
|
||||||
|
if (addTitle && !el.title) {
|
||||||
|
el.title = el.textContent;
|
||||||
|
} else if (!addTitle && el.title) {
|
||||||
|
el.title = '';
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
if (element) element.classList.add(TARGET_CLASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// scrolls the editor to reveal the match
|
||||||
|
function makeMatchVisible(cm, searchCursor) {
|
||||||
|
const canFocus = !state.dialog || !state.dialog.contains(document.activeElement);
|
||||||
|
state.cm = cm;
|
||||||
|
|
||||||
|
// scroll within the editor
|
||||||
|
Object.assign(getStateSafe(cm), {
|
||||||
|
cursorPos: {
|
||||||
|
from: cm.getCursor('from'),
|
||||||
|
to: cm.getCursor('to'),
|
||||||
|
},
|
||||||
|
searchPos: searchCursor.pos,
|
||||||
|
unclosedOp: !cm.curOp,
|
||||||
|
});
|
||||||
|
if (!cm.curOp) cm.startOperation();
|
||||||
|
if (canFocus) cm.setSelection(searchCursor.pos.from, searchCursor.pos.to);
|
||||||
|
cm.scrollIntoView(searchCursor.pos, SCROLL_REVEAL_MIN_PX);
|
||||||
|
|
||||||
|
// scroll to the editor itself
|
||||||
|
makeSectionVisible(cm);
|
||||||
|
|
||||||
|
// focus or expose as the current search target
|
||||||
|
clearMarker();
|
||||||
|
if (canFocus) {
|
||||||
|
cm.focus();
|
||||||
|
makeTargetVisible(null);
|
||||||
|
} else {
|
||||||
|
makeTargetVisible(cm.display.wrapper);
|
||||||
|
// mark the match
|
||||||
|
const pos = searchCursor.pos;
|
||||||
|
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 (num === undefined) {
|
||||||
|
num = 0;
|
||||||
|
for (const cm of 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) : [];
|
||||||
|
for (const el of elements) {
|
||||||
|
const value = el.value;
|
||||||
|
if (state.rx) {
|
||||||
|
state.rx.lastIndex = 0;
|
||||||
|
while (state.rx.exec(value)) numApplies++;
|
||||||
|
} 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 focusNoScroll(el) {
|
||||||
|
if (el) {
|
||||||
|
saveWindowScrollPos();
|
||||||
|
el.focus({preventScroll: true});
|
||||||
|
restoreWindowScrollPos({immediately: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function toggleDataset(el, prop, state) {
|
||||||
|
if (state) {
|
||||||
|
el.dataset[prop] = '';
|
||||||
|
} else {
|
||||||
|
delete el.dataset[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function saveWindowScrollPos() {
|
||||||
|
state.scrollX = window.scrollX;
|
||||||
|
state.scrollY = window.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function restoreWindowScrollPos({immediately = true} = {}) {
|
||||||
|
if (window.scrollX !== state.scrollX || window.scrollY !== state.scrollY) {
|
||||||
|
invokeOrPostpone(immediately, window.scrollTo, 0, state.scrollX, state.scrollY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// produces [i, i+1, i-1, i+2, i-2, i+3, i-3, ...]
|
||||||
|
function radiateArray(arr, focalIndex) {
|
||||||
|
const result = [arr[focalIndex]];
|
||||||
|
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() {
|
||||||
|
chrome.storage.local.get('editor', ({editor = {}}) => {
|
||||||
|
state.find = editor.find || '';
|
||||||
|
state.replace = editor.replace || '';
|
||||||
|
state.icase = editor.icase || state.icase;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function writeStorage() {
|
||||||
|
chrome.storage.local.get('editor', ({editor}) =>
|
||||||
|
chrome.storage.local.set({
|
||||||
|
editor: Object.assign(editor || {}, {
|
||||||
|
find: state.find,
|
||||||
|
replace: state.replace,
|
||||||
|
icase: state.icase,
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
252
vendor/codemirror/addon/search/search.js
vendored
252
vendor/codemirror/addon/search/search.js
vendored
|
@ -1,252 +0,0 @@
|
||||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
|
||||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
|
||||||
|
|
||||||
// Define search commands. Depends on dialog.js or another
|
|
||||||
// implementation of the openDialog method.
|
|
||||||
|
|
||||||
// Replace works a little oddly -- it will do the replace on the next
|
|
||||||
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
|
|
||||||
// replace by making sure the match is no longer selected when hitting
|
|
||||||
// Ctrl-G.
|
|
||||||
|
|
||||||
(function(mod) {
|
|
||||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
|
||||||
mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog"));
|
|
||||||
else if (typeof define == "function" && define.amd) // AMD
|
|
||||||
define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
|
|
||||||
else // Plain browser env
|
|
||||||
mod(CodeMirror);
|
|
||||||
})(function(CodeMirror) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
function searchOverlay(query, caseInsensitive) {
|
|
||||||
if (typeof query == "string")
|
|
||||||
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
|
|
||||||
else if (!query.global)
|
|
||||||
query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
|
|
||||||
|
|
||||||
return {token: function(stream) {
|
|
||||||
query.lastIndex = stream.pos;
|
|
||||||
var match = query.exec(stream.string);
|
|
||||||
if (match && match.index == stream.pos) {
|
|
||||||
stream.pos += match[0].length || 1;
|
|
||||||
return "searching";
|
|
||||||
} else if (match) {
|
|
||||||
stream.pos = match.index;
|
|
||||||
} else {
|
|
||||||
stream.skipToEnd();
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchState() {
|
|
||||||
this.posFrom = this.posTo = this.lastQuery = this.query = null;
|
|
||||||
this.overlay = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSearchState(cm) {
|
|
||||||
return cm.state.search || (cm.state.search = new SearchState());
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryCaseInsensitive(query) {
|
|
||||||
return typeof query == "string" && query == query.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSearchCursor(cm, query, pos) {
|
|
||||||
// Heuristic: if the query string is all lowercase, do a case insensitive search.
|
|
||||||
return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
|
|
||||||
cm.openDialog(text, onEnter, {
|
|
||||||
value: deflt,
|
|
||||||
selectValueOnOpen: true,
|
|
||||||
closeOnEnter: false,
|
|
||||||
onClose: function() { clearSearch(cm); },
|
|
||||||
onKeyDown: onKeyDown
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function dialog(cm, text, shortText, deflt, f) {
|
|
||||||
if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
|
|
||||||
else f(prompt(shortText, deflt));
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmDialog(cm, text, shortText, fs) {
|
|
||||||
if (cm.openConfirm) cm.openConfirm(text, fs);
|
|
||||||
else if (confirm(shortText)) fs[0]();
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseString(string) {
|
|
||||||
return string.replace(/\\(.)/g, function(_, ch) {
|
|
||||||
if (ch == "n") return "\n"
|
|
||||||
if (ch == "r") return "\r"
|
|
||||||
return ch
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseQuery(query) {
|
|
||||||
var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
|
|
||||||
if (isRE) {
|
|
||||||
try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
|
|
||||||
catch(e) {} // Not a regular expression after all, do a string search
|
|
||||||
} else {
|
|
||||||
query = parseString(query)
|
|
||||||
}
|
|
||||||
if (typeof query == "string" ? query == "" : query.test(""))
|
|
||||||
query = /x^/;
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryDialog =
|
|
||||||
'<span class="CodeMirror-search-label">Search:</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
|
|
||||||
|
|
||||||
function startSearch(cm, state, query) {
|
|
||||||
state.queryText = query;
|
|
||||||
state.query = parseQuery(query);
|
|
||||||
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
|
|
||||||
state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
|
|
||||||
cm.addOverlay(state.overlay);
|
|
||||||
if (cm.showMatchesOnScrollbar) {
|
|
||||||
if (state.annotate) { state.annotate.clear(); state.annotate = null; }
|
|
||||||
state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSearch(cm, rev, persistent, immediate) {
|
|
||||||
var state = getSearchState(cm);
|
|
||||||
if (state.query) return findNext(cm, rev);
|
|
||||||
var q = cm.getSelection() || state.lastQuery;
|
|
||||||
if (q instanceof RegExp && q.source == "x^") q = null
|
|
||||||
if (persistent && cm.openDialog) {
|
|
||||||
var hiding = null
|
|
||||||
var searchNext = function(query, event) {
|
|
||||||
CodeMirror.e_stop(event);
|
|
||||||
if (!query) return;
|
|
||||||
if (query != state.queryText) {
|
|
||||||
startSearch(cm, state, query);
|
|
||||||
state.posFrom = state.posTo = cm.getCursor();
|
|
||||||
}
|
|
||||||
if (hiding) hiding.style.opacity = 1
|
|
||||||
findNext(cm, event.shiftKey, function(_, to) {
|
|
||||||
var dialog
|
|
||||||
if (to.line < 3 && document.querySelector &&
|
|
||||||
(dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
|
|
||||||
dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
|
|
||||||
(hiding = dialog).style.opacity = .4
|
|
||||||
})
|
|
||||||
};
|
|
||||||
persistentDialog(cm, queryDialog, q, searchNext, function(event, query) {
|
|
||||||
var keyName = CodeMirror.keyName(event)
|
|
||||||
var extra = cm.getOption('extraKeys'), cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption("keyMap")][keyName]
|
|
||||||
if (cmd == "findNext" || cmd == "findPrev" ||
|
|
||||||
cmd == "findPersistentNext" || cmd == "findPersistentPrev") {
|
|
||||||
CodeMirror.e_stop(event);
|
|
||||||
startSearch(cm, getSearchState(cm), query);
|
|
||||||
cm.execCommand(cmd);
|
|
||||||
} else if (cmd == "find" || cmd == "findPersistent") {
|
|
||||||
CodeMirror.e_stop(event);
|
|
||||||
searchNext(query, event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (immediate && q) {
|
|
||||||
startSearch(cm, state, q);
|
|
||||||
findNext(cm, rev);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dialog(cm, queryDialog, "Search for:", q, function(query) {
|
|
||||||
if (query && !state.query) cm.operation(function() {
|
|
||||||
startSearch(cm, state, query);
|
|
||||||
state.posFrom = state.posTo = cm.getCursor();
|
|
||||||
findNext(cm, rev);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findNext(cm, rev, callback) {cm.operation(function() {
|
|
||||||
var state = getSearchState(cm);
|
|
||||||
var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
|
|
||||||
if (!cursor.find(rev)) {
|
|
||||||
cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
|
|
||||||
if (!cursor.find(rev)) return;
|
|
||||||
}
|
|
||||||
cm.setSelection(cursor.from(), cursor.to());
|
|
||||||
cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
|
|
||||||
state.posFrom = cursor.from(); state.posTo = cursor.to();
|
|
||||||
if (callback) callback(cursor.from(), cursor.to())
|
|
||||||
});}
|
|
||||||
|
|
||||||
function clearSearch(cm) {cm.operation(function() {
|
|
||||||
var state = getSearchState(cm);
|
|
||||||
state.lastQuery = state.query;
|
|
||||||
if (!state.query) return;
|
|
||||||
state.query = state.queryText = null;
|
|
||||||
cm.removeOverlay(state.overlay);
|
|
||||||
if (state.annotate) { state.annotate.clear(); state.annotate = null; }
|
|
||||||
});}
|
|
||||||
|
|
||||||
var replaceQueryDialog =
|
|
||||||
' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
|
|
||||||
var replacementQueryDialog = '<span class="CodeMirror-search-label">With:</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
|
|
||||||
var doReplaceConfirm = '<span class="CodeMirror-search-label">Replace?</span> <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>';
|
|
||||||
|
|
||||||
function replaceAll(cm, query, text) {
|
|
||||||
cm.operation(function() {
|
|
||||||
for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
|
|
||||||
if (typeof query != "string") {
|
|
||||||
var match = cm.getRange(cursor.from(), cursor.to()).match(query);
|
|
||||||
cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
|
|
||||||
} else cursor.replace(text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function replace(cm, all) {
|
|
||||||
if (cm.getOption("readOnly")) return;
|
|
||||||
var query = cm.getSelection() || getSearchState(cm).lastQuery;
|
|
||||||
var dialogText = '<span class="CodeMirror-search-label">' + (all ? 'Replace all:' : 'Replace:') + '</span>';
|
|
||||||
dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
|
|
||||||
if (!query) return;
|
|
||||||
query = parseQuery(query);
|
|
||||||
dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
|
|
||||||
text = parseString(text)
|
|
||||||
if (all) {
|
|
||||||
replaceAll(cm, query, text)
|
|
||||||
} else {
|
|
||||||
clearSearch(cm);
|
|
||||||
var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
|
|
||||||
var advance = function() {
|
|
||||||
var start = cursor.from(), match;
|
|
||||||
if (!(match = cursor.findNext())) {
|
|
||||||
cursor = getSearchCursor(cm, query);
|
|
||||||
if (!(match = cursor.findNext()) ||
|
|
||||||
(start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
|
|
||||||
}
|
|
||||||
cm.setSelection(cursor.from(), cursor.to());
|
|
||||||
cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
|
|
||||||
confirmDialog(cm, doReplaceConfirm, "Replace?",
|
|
||||||
[function() {doReplace(match);}, advance,
|
|
||||||
function() {replaceAll(cm, query, text)}]);
|
|
||||||
};
|
|
||||||
var doReplace = function(match) {
|
|
||||||
cursor.replace(typeof query == "string" ? text :
|
|
||||||
text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
|
|
||||||
advance();
|
|
||||||
};
|
|
||||||
advance();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
|
|
||||||
CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
|
|
||||||
CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};
|
|
||||||
CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};
|
|
||||||
CodeMirror.commands.findNext = doSearch;
|
|
||||||
CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
|
|
||||||
CodeMirror.commands.clearSearch = clearSearch;
|
|
||||||
CodeMirror.commands.replace = replace;
|
|
||||||
CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user