code.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <template>
  2. <div class="flex flex-col">
  3. <div class="frame-block" v-show="search.show">
  4. <button class="pull-right" @click="clearSearch">&times;</button>
  5. <form class="inline-block mr-1" @submit.prevent="goToLine()">
  6. <span v-text="i18n('labelLineNumber')"></span>
  7. <input class="w-1" v-model="search.line">
  8. </form>
  9. <form class="inline-block mr-1" @submit.prevent="findNext()">
  10. <span v-text="i18n('labelSearch')"></span>
  11. <tooltip title="Ctrl-F">
  12. <input ref="search" v-model="search.state.query">
  13. </tooltip>
  14. <tooltip title="Shift-Ctrl-G">
  15. <button type="button" @click="findNext(1)">&lt;</button>
  16. </tooltip>
  17. <tooltip title="Ctrl-G">
  18. <button type="submit">&gt;</button>
  19. </tooltip>
  20. </form>
  21. <form class="inline-block mr-1" @submit.prevent="replace()" v-if="!readonly">
  22. <span v-text="i18n('labelReplace')"></span>
  23. <input v-model="search.state.replace">
  24. <tooltip title="Shift-Ctrl-F">
  25. <button type="submit" v-text="i18n('buttonReplace')"></button>
  26. </tooltip>
  27. <tooltip title="Shift-Ctrl-R">
  28. <button type="button" v-text="i18n('buttonReplaceAll')" @click="replace(1)"></button>
  29. </tooltip>
  30. </form>
  31. <div class="inline-block">
  32. <tooltip :title="i18n('searchUseRegex')">
  33. <toggle-button v-model="searchOptions.useRegex">.*</toggle-button>
  34. </tooltip>
  35. <tooltip :title="i18n('searchCaseSensitive')">
  36. <toggle-button v-model="searchOptions.caseSensitive">Aa</toggle-button>
  37. </tooltip>
  38. </div>
  39. </div>
  40. <vl-code
  41. class="editor-code flex-auto"
  42. :options="cmOptions" v-model="content" @ready="onReady"
  43. />
  44. </div>
  45. </template>
  46. <script>
  47. import 'codemirror/lib/codemirror.css';
  48. import 'codemirror/theme/eclipse.css';
  49. import 'codemirror/mode/javascript/javascript';
  50. import 'codemirror/addon/comment/continuecomment';
  51. import 'codemirror/addon/edit/matchbrackets';
  52. import 'codemirror/addon/edit/closebrackets';
  53. import 'codemirror/addon/fold/foldcode';
  54. import 'codemirror/addon/fold/foldgutter';
  55. import 'codemirror/addon/fold/brace-fold';
  56. import 'codemirror/addon/fold/comment-fold';
  57. import 'codemirror/addon/search/match-highlighter';
  58. import 'codemirror/addon/search/searchcursor';
  59. import 'codemirror/addon/selection/active-line';
  60. import CodeMirror from 'codemirror';
  61. import VlCode from 'vueleton/lib/code';
  62. import Tooltip from 'vueleton/lib/tooltip';
  63. import { debounce } from 'src/common';
  64. import ToggleButton from 'src/common/ui/toggle-button';
  65. function getHandler(key) {
  66. return cm => {
  67. const { commands } = cm.state;
  68. const handle = commands && commands[key];
  69. return handle && handle();
  70. };
  71. }
  72. [
  73. 'save', 'cancel', 'close',
  74. 'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
  75. ].forEach(key => {
  76. CodeMirror.commands[key] = getHandler(key);
  77. });
  78. Object.assign(CodeMirror.keyMap.default, {
  79. Tab: 'indentMore',
  80. 'Shift-Tab': 'indentLess',
  81. });
  82. const cmOptions = {
  83. continueComments: true,
  84. matchBrackets: true,
  85. autoCloseBrackets: true,
  86. highlightSelectionMatches: true,
  87. styleActiveLine: true,
  88. foldGutter: true,
  89. gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
  90. theme: 'eclipse',
  91. };
  92. const searchOptions = {
  93. useRegex: false,
  94. caseSensitive: false,
  95. };
  96. function findNext(cm, state, reversed) {
  97. cm.operation(() => {
  98. let query = state.query || '';
  99. if (query && searchOptions.useRegex) {
  100. query = new RegExp(query, searchOptions.caseSensitive ? '' : 'i');
  101. }
  102. const options = {
  103. caseFold: !searchOptions.caseSensitive,
  104. };
  105. let cursor = cm.getSearchCursor(query, reversed ? state.posFrom : state.posTo, options);
  106. if (!cursor.find(reversed)) {
  107. cursor = cm.getSearchCursor(
  108. query,
  109. reversed ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0),
  110. options,
  111. );
  112. if (!cursor.find(reversed)) return;
  113. }
  114. cm.setSelection(cursor.from(), cursor.to());
  115. state.posFrom = cursor.from();
  116. state.posTo = cursor.to();
  117. });
  118. }
  119. function replaceOne(cm, state) {
  120. const start = cm.getCursor('start');
  121. const end = cm.getCursor('end');
  122. state.posTo = state.posFrom;
  123. findNext(cm, state);
  124. const start2 = cm.getCursor('start');
  125. const end2 = cm.getCursor('end');
  126. if (
  127. start.line === start2.line && start.ch === start2.ch
  128. && end.line === end2.line && end.ch === end2.ch
  129. ) {
  130. cm.replaceRange(state.replace, start, end);
  131. findNext(cm, state);
  132. }
  133. }
  134. function replaceAll(cm, state) {
  135. cm.operation(() => {
  136. const query = state.query || '';
  137. for (let cursor = cm.getSearchCursor(query); cursor.findNext();) {
  138. cursor.replace(state.replace);
  139. }
  140. });
  141. }
  142. export default {
  143. props: {
  144. readonly: {
  145. type: Boolean,
  146. default: false,
  147. },
  148. value: true,
  149. commands: true,
  150. global: {
  151. type: Boolean,
  152. default: true,
  153. },
  154. },
  155. components: {
  156. VlCode,
  157. Tooltip,
  158. ToggleButton,
  159. },
  160. data() {
  161. return {
  162. cmOptions,
  163. searchOptions,
  164. content: null,
  165. lineTooLong: false,
  166. search: {
  167. show: false,
  168. state: {
  169. query: null,
  170. replace: null,
  171. },
  172. },
  173. };
  174. },
  175. watch: {
  176. content(content) {
  177. this.$emit('input', content);
  178. },
  179. value(value) {
  180. if (value === this.content) return;
  181. const { cut, cutLines } = this.getCutContent(value);
  182. this.lineTooLong = cut && cutLines;
  183. this.checkOptions();
  184. this.content = cut ? cutLines.map(({ text }) => text).join('\n') : value;
  185. this.$emit('warnLarge', !!this.lineTooLong);
  186. const { cm } = this;
  187. if (!cm) return;
  188. this.$nextTick(() => {
  189. cm.getDoc().clearHistory();
  190. cm.focus();
  191. });
  192. },
  193. 'search.state.query'() {
  194. this.debouncedFind();
  195. },
  196. searchOptions: {
  197. deep: true,
  198. handler() {
  199. this.debouncedFind();
  200. },
  201. },
  202. },
  203. methods: {
  204. checkOptions() {
  205. const { cm, lineTooLong } = this;
  206. if (!cm) return;
  207. cm.setOption('readOnly', !!(lineTooLong || this.readonly));
  208. cm.setOption('mode', lineTooLong ? 'null' : 'javascript');
  209. cm.setOption('lineNumbers', !lineTooLong);
  210. cm.setOption('lineWrapping', !lineTooLong);
  211. },
  212. getCutContent(value) {
  213. const lines = value.split('\n');
  214. const cut = lines.some(line => line.length > 10 * 1024);
  215. const cutLines = [];
  216. if (cut) {
  217. const maxLength = 3 * 1024;
  218. lines.forEach((line, index) => {
  219. for (let offset = 0; offset < line.length; offset += maxLength) {
  220. cutLines.push({
  221. index,
  222. text: line.slice(offset, offset + maxLength),
  223. });
  224. }
  225. if (!line.length) {
  226. cutLines.push({
  227. index,
  228. text: '',
  229. });
  230. }
  231. });
  232. }
  233. return { cut, cutLines };
  234. },
  235. onReady(cm) {
  236. this.cm = cm;
  237. this.checkOptions();
  238. cm.state.commands = Object.assign({
  239. cancel: () => {
  240. if (this.search.show) {
  241. this.clearSearch();
  242. } else {
  243. cm.execCommand('close');
  244. }
  245. },
  246. find: this.find,
  247. findNext: this.findNext,
  248. findPrev: () => {
  249. this.findNext(1);
  250. },
  251. replace: this.replace,
  252. replaceAll: () => {
  253. this.replace(1);
  254. },
  255. }, this.commands);
  256. cm.setOption('extraKeys', {
  257. Esc: 'cancel',
  258. });
  259. cm.on('keyHandled', (_cm, _name, e) => {
  260. e.stopPropagation();
  261. });
  262. this.$emit('ready', cm);
  263. },
  264. onKeyDown(e) {
  265. const name = CodeMirror.keyName(e);
  266. const { cm } = this;
  267. if (!cm) return;
  268. [
  269. cm.options.extraKeys,
  270. cm.options.keyMap,
  271. ].some(keyMap => {
  272. let stop = false;
  273. if (keyMap) {
  274. CodeMirror.lookupKey(name, keyMap, b => {
  275. if (cm.state.commands[b]) {
  276. e.preventDefault();
  277. e.stopPropagation();
  278. cm.execCommand(b);
  279. stop = true;
  280. }
  281. }, cm);
  282. }
  283. return stop;
  284. });
  285. },
  286. doSearch(reversed) {
  287. const { state } = this.search;
  288. const { cm } = this;
  289. if (state.query) {
  290. findNext(cm, state, reversed);
  291. }
  292. this.search.show = true;
  293. },
  294. searchInPlace() {
  295. const { state } = this.search;
  296. state.posTo = state.posFrom;
  297. this.doSearch();
  298. },
  299. find() {
  300. this.searchInPlace();
  301. this.$nextTick(() => {
  302. const { search } = this.$refs;
  303. search.select();
  304. search.focus();
  305. });
  306. },
  307. findNext(reversed) {
  308. this.doSearch(reversed);
  309. this.$nextTick(() => {
  310. this.$refs.search.focus();
  311. });
  312. },
  313. clearSearch() {
  314. const { cm } = this;
  315. cm.operation(() => {
  316. const { state } = this.search;
  317. state.posFrom = null;
  318. state.posTo = null;
  319. this.search.show = false;
  320. });
  321. cm.focus();
  322. },
  323. replace(all) {
  324. const { cm } = this;
  325. const { state } = this.search;
  326. if (!state.query) {
  327. this.find();
  328. return;
  329. }
  330. (all ? replaceAll : replaceOne)(cm, state);
  331. },
  332. goToLine() {
  333. const { cm } = this;
  334. const line = +this.search.line;
  335. if (line > 0) cm.setCursor(line - 1, 0);
  336. cm.focus();
  337. },
  338. onCopy(e) {
  339. if (!this.lineTooLong || !this.cm || !this.cm.somethingSelected()) return;
  340. const [rng] = this.cm.listSelections();
  341. const positions = {};
  342. [rng.anchor, rng.head].forEach(pos => {
  343. positions[pos.sticky] = pos;
  344. });
  345. const meta = [];
  346. {
  347. let { line, ch } = positions.after;
  348. for (; line < positions.before.line; line += 1) {
  349. meta.push({ line, from: ch });
  350. ch = 0;
  351. }
  352. meta.push({ line, from: ch, to: positions.before.ch });
  353. }
  354. const result = [];
  355. let lastLine;
  356. meta.forEach(({ line, from, to }) => {
  357. const { text, index } = this.lineTooLong[line];
  358. if (lastLine != null && lastLine !== index) {
  359. result.push('\n');
  360. }
  361. lastLine = index;
  362. result.push(to == null ? text.slice(from) : text.slice(from, to));
  363. });
  364. e.clipboardData.setData('text', result.join(''));
  365. e.preventDefault();
  366. e.stopImmediatePropagation();
  367. },
  368. },
  369. mounted() {
  370. this.debouncedFind = debounce(this.searchInPlace, 100);
  371. if (this.global) window.addEventListener('keydown', this.onKeyDown, false);
  372. document.addEventListener('copy', this.onCopy, false);
  373. },
  374. beforeDestroy() {
  375. if (this.global) window.removeEventListener('keydown', this.onKeyDown, false);
  376. document.removeEventListener('copy', this.onCopy, false);
  377. },
  378. };
  379. </script>
  380. <style>
  381. /* compatible with old browsers, e.g. Maxthon 4.4, Chrome 50- */
  382. .editor-code.flex-auto {
  383. position: relative;
  384. > div {
  385. position: absolute;
  386. width: 100%;
  387. }
  388. }
  389. </style>