|
|
@@ -1,7 +1,38 @@
|
|
|
<template>
|
|
|
- <vue-code class="editor-code"
|
|
|
- :options="cmOptions" v-model="content" @ready="onReady"
|
|
|
- />
|
|
|
+ <div class="flex flex-col">
|
|
|
+ <vue-code class="editor-code flex-auto"
|
|
|
+ :options="cmOptions" v-model="content" @ready="onReady"
|
|
|
+ />
|
|
|
+ <div class="frame-block" v-show="search.show">
|
|
|
+ <button class="pull-right" @click="clearSearch">×</button>
|
|
|
+ <form class="inline-block mr-1" @submit.prevent="goToLine()">
|
|
|
+ <span v-text="i18n('labelLineNumber')"></span>
|
|
|
+ <input class="w-1" v-model="search.line">
|
|
|
+ </form>
|
|
|
+ <form class="inline-block mr-1" @submit.prevent="findNext()">
|
|
|
+ <span v-text="i18n('labelSearch')"></span>
|
|
|
+ <tooltip title="Ctrl-F">
|
|
|
+ <input ref="search" v-model="search.state.query">
|
|
|
+ </tooltip>
|
|
|
+ <tooltip title="Shift-Ctrl-G">
|
|
|
+ <button type="button" @click="findNext(1)"><</button>
|
|
|
+ </tooltip>
|
|
|
+ <tooltip title="Ctrl-G">
|
|
|
+ <button type="submit">></button>
|
|
|
+ </tooltip>
|
|
|
+ </form>
|
|
|
+ <form class="inline-block mr-1" @submit.prevent="replace()" v-if="!readonly">
|
|
|
+ <span v-text="i18n('labelReplace')"></span>
|
|
|
+ <input v-model="search.state.replace">
|
|
|
+ <tooltip title="Shift-Ctrl-F">
|
|
|
+ <button type="submit" v-text="i18n('buttonReplace')"></button>
|
|
|
+ </tooltip>
|
|
|
+ <tooltip title="Shift-Ctrl-R">
|
|
|
+ <button type="button" v-text="i18n('buttonReplaceAll')" @click="replace(1)"></button>
|
|
|
+ </tooltip>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
@@ -20,6 +51,7 @@ import 'codemirror/addon/search/match-highlighter';
|
|
|
import 'codemirror/addon/search/searchcursor';
|
|
|
import 'codemirror/addon/selection/active-line';
|
|
|
import CodeMirror from 'codemirror';
|
|
|
+import { debounce } from 'src/common';
|
|
|
|
|
|
function getHandler(key) {
|
|
|
return (cm) => {
|
|
|
@@ -39,7 +71,7 @@ function indentWithTab(cm) {
|
|
|
}
|
|
|
|
|
|
[
|
|
|
- 'save', 'cancel', 'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
|
|
|
+ 'save', 'cancel', 'find', 'findNext', 'findPrev', 'replace', 'replaceAll', 'close',
|
|
|
].forEach((key) => {
|
|
|
CodeMirror.commands[key] = getHandler(key);
|
|
|
});
|
|
|
@@ -58,6 +90,44 @@ const cmOptions = {
|
|
|
theme: 'eclipse',
|
|
|
};
|
|
|
|
|
|
+function findNext(cm, state, reversed) {
|
|
|
+ cm.operation(() => {
|
|
|
+ const query = state.query || '';
|
|
|
+ let cursor = cm.getSearchCursor(query, reversed ? state.posFrom : state.posTo);
|
|
|
+ if (!cursor.find(reversed)) {
|
|
|
+ cursor = cm.getSearchCursor(query,
|
|
|
+ reversed ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
|
|
|
+ if (!cursor.find(reversed)) return;
|
|
|
+ }
|
|
|
+ cm.setSelection(cursor.from(), cursor.to());
|
|
|
+ state.posFrom = cursor.from();
|
|
|
+ state.posTo = cursor.to();
|
|
|
+ });
|
|
|
+}
|
|
|
+function replaceOne(cm, state) {
|
|
|
+ const start = cm.getCursor('start');
|
|
|
+ const end = cm.getCursor('end');
|
|
|
+ state.posTo = state.posFrom;
|
|
|
+ findNext(cm, state);
|
|
|
+ const start2 = cm.getCursor('start');
|
|
|
+ const end2 = cm.getCursor('end');
|
|
|
+ if (
|
|
|
+ start.line === start2.line && start.ch === start2.ch
|
|
|
+ && end.line === end2.line && end.ch === end2.ch
|
|
|
+ ) {
|
|
|
+ cm.replaceRange(state.replace, start, end);
|
|
|
+ findNext(cm, state);
|
|
|
+ }
|
|
|
+}
|
|
|
+function replaceAll(cm, state) {
|
|
|
+ cm.operation(() => {
|
|
|
+ const query = state.query || '';
|
|
|
+ for (let cursor = cm.getSearchCursor(query); cursor.findNext();) {
|
|
|
+ cursor.replace(state.replace);
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
export default {
|
|
|
props: {
|
|
|
readonly: {
|
|
|
@@ -76,8 +146,15 @@ export default {
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
- content: this.value,
|
|
|
cmOptions,
|
|
|
+ content: this.value,
|
|
|
+ search: {
|
|
|
+ show: false,
|
|
|
+ state: {
|
|
|
+ query: null,
|
|
|
+ replace: null,
|
|
|
+ },
|
|
|
+ },
|
|
|
};
|
|
|
},
|
|
|
watch: {
|
|
|
@@ -92,12 +169,32 @@ export default {
|
|
|
cm.getDoc().clearHistory();
|
|
|
cm.focus();
|
|
|
},
|
|
|
+ 'search.state.query'() {
|
|
|
+ this.debouncedFind();
|
|
|
+ },
|
|
|
},
|
|
|
methods: {
|
|
|
onReady(cm) {
|
|
|
this.cm = cm;
|
|
|
if (this.readonly) cm.setOption('readOnly', true);
|
|
|
- cm.state.commands = this.commands;
|
|
|
+ cm.state.commands = Object.assign({
|
|
|
+ cancel: () => {
|
|
|
+ if (this.search.show) {
|
|
|
+ this.clearSearch();
|
|
|
+ } else {
|
|
|
+ cm.execCommand('close');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ find: this.find,
|
|
|
+ findNext: this.findNext,
|
|
|
+ findPrev: () => {
|
|
|
+ this.findNext(1);
|
|
|
+ },
|
|
|
+ replace: this.replace,
|
|
|
+ replaceAll: () => {
|
|
|
+ this.replace(1);
|
|
|
+ },
|
|
|
+ }, this.commands);
|
|
|
cm.setOption('extraKeys', {
|
|
|
Esc: 'cancel',
|
|
|
Tab: indentWithTab,
|
|
|
@@ -117,7 +214,7 @@ export default {
|
|
|
let stop = false;
|
|
|
if (keyMap) {
|
|
|
CodeMirror.lookupKey(name, keyMap, (b) => {
|
|
|
- if (this.commands[b]) {
|
|
|
+ if (cm.state.commands[b]) {
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
cm.execCommand(b);
|
|
|
@@ -128,8 +225,58 @@ export default {
|
|
|
return stop;
|
|
|
});
|
|
|
},
|
|
|
+ doFind(reversed) {
|
|
|
+ const { state } = this.search;
|
|
|
+ const { cm } = this;
|
|
|
+ if (state.query) {
|
|
|
+ findNext(cm, state, reversed);
|
|
|
+ }
|
|
|
+ this.search.show = true;
|
|
|
+ },
|
|
|
+ find() {
|
|
|
+ const { state } = this.search;
|
|
|
+ state.posTo = state.posFrom;
|
|
|
+ this.doFind();
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const { search } = this.$refs;
|
|
|
+ search.select();
|
|
|
+ search.focus();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ findNext(reversed) {
|
|
|
+ this.doFind(reversed);
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.$refs.search.focus();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ clearSearch() {
|
|
|
+ const { cm } = this;
|
|
|
+ cm.operation(() => {
|
|
|
+ const { state } = this.search;
|
|
|
+ state.posFrom = null;
|
|
|
+ state.posTo = null;
|
|
|
+ this.search.show = false;
|
|
|
+ });
|
|
|
+ cm.focus();
|
|
|
+ },
|
|
|
+ replace(all) {
|
|
|
+ const { cm } = this;
|
|
|
+ const { state } = this.search;
|
|
|
+ if (!state.query) {
|
|
|
+ this.find();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ (all ? replaceAll : replaceOne)(cm, state);
|
|
|
+ },
|
|
|
+ goToLine() {
|
|
|
+ const line = this.search.line - 1;
|
|
|
+ const { cm } = this;
|
|
|
+ if (!isNaN(line)) cm.setCursor(line, 0);
|
|
|
+ cm.focus();
|
|
|
+ },
|
|
|
},
|
|
|
mounted() {
|
|
|
+ this.debouncedFind = debounce(this.doFind, 100);
|
|
|
if (this.global) window.addEventListener('keydown', this.onKeyDown, false);
|
|
|
},
|
|
|
beforeDestroy() {
|