// CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://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) // Plain browser env else mod(CodeMirror) })(function (CodeMirror) { 'use strict' // default search panel location CodeMirror.defineOption('search', { bottom: false }) 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, bottom: cm.options.search.bottom, }) } function dialog(cm, text, shortText, deflt, f) { if (cm.openDialog) cm.openDialog(text, f, { value: deflt, selectValueOnOpen: true, bottom: cm.options.search.bottom }) 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(/\\([nrt\\])/g, function (match, ch) { if (ch == 'n') return '\n' if (ch == 'r') return '\r' if (ch == 't') return '\t' if (ch == '\\') return '\\' return match }) } 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 } 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 = 0.4 }) } persistentDialog(cm, getQueryDialog(cm), 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, getQueryDialog(cm), '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 } }) } function el(tag, attrs) { var element = tag ? document.createElement(tag) : document.createDocumentFragment() for (var key in attrs) { element[key] = attrs[key] } for (var i = 2; i < arguments.length; i++) { var child = arguments[i] element.appendChild(typeof child == 'string' ? document.createTextNode(child) : child) } return element } function getQueryDialog(cm) { return el( '', null, el('span', { className: 'CodeMirror-search-label' }, cm.phrase('Search:')), ' ', el('input', { type: 'text', style: 'width: 10em', className: 'CodeMirror-search-field' }), ' ', el('span', { style: 'color: #888', className: 'CodeMirror-search-hint' }, cm.phrase('(Use /re/ syntax for regexp search)')) ) } function getReplaceQueryDialog(cm) { return el( '', null, ' ', el('input', { type: 'text', style: 'width: 10em', className: 'CodeMirror-search-field' }), ' ', el('span', { style: 'color: #888', className: 'CodeMirror-search-hint' }, cm.phrase('(Use /re/ syntax for regexp search)')) ) } function getReplacementQueryDialog(cm) { return el('', null, el('span', { className: 'CodeMirror-search-label' }, cm.phrase('With:')), ' ', el('input', { type: 'text', style: 'width: 10em', className: 'CodeMirror-search-field' })) } function getDoReplaceConfirm(cm) { return el( '', null, el('span', { className: 'CodeMirror-search-label' }, cm.phrase('Replace?')), ' ', el('button', {}, cm.phrase('Yes')), ' ', el('button', {}, cm.phrase('No')), ' ', el('button', {}, cm.phrase('All')), ' ', el('button', {}, cm.phrase('Stop')) ) } 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 = all ? cm.phrase('Replace all:') : cm.phrase('Replace:') var fragment = el('', null, el('span', { className: 'CodeMirror-search-label' }, dialogText), getReplaceQueryDialog(cm)) dialog(cm, fragment, dialogText, query, function (query) { if (!query) return query = parseQuery(query) dialog(cm, getReplacementQueryDialog(cm), cm.phrase('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, getDoReplaceConfirm(cm), cm.phrase('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) } })