smartSelect.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  6. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  7. return new (P || (P = Promise))(function (resolve, reject) {
  8. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  9. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  10. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  11. step((generator = generator.apply(thisArg, _arguments || [])).next());
  12. });
  13. };
  14. import * as arrays from '../../../base/common/arrays.js';
  15. import { CancellationToken } from '../../../base/common/cancellation.js';
  16. import { onUnexpectedExternalError } from '../../../base/common/errors.js';
  17. import { EditorAction, registerEditorAction, registerEditorContribution, registerModelCommand } from '../../browser/editorExtensions.js';
  18. import { Position } from '../../common/core/position.js';
  19. import { Range } from '../../common/core/range.js';
  20. import { Selection } from '../../common/core/selection.js';
  21. import { EditorContextKeys } from '../../common/editorContextKeys.js';
  22. import * as modes from '../../common/modes.js';
  23. import { BracketSelectionRangeProvider } from './bracketSelections.js';
  24. import { WordSelectionRangeProvider } from './wordSelections.js';
  25. import * as nls from '../../../nls.js';
  26. import { MenuId } from '../../../platform/actions/common/actions.js';
  27. import { CommandsRegistry } from '../../../platform/commands/common/commands.js';
  28. class SelectionRanges {
  29. constructor(index, ranges) {
  30. this.index = index;
  31. this.ranges = ranges;
  32. }
  33. mov(fwd) {
  34. let index = this.index + (fwd ? 1 : -1);
  35. if (index < 0 || index >= this.ranges.length) {
  36. return this;
  37. }
  38. const res = new SelectionRanges(index, this.ranges);
  39. if (res.ranges[index].equalsRange(this.ranges[this.index])) {
  40. // next range equals this range, retry with next-next
  41. return res.mov(fwd);
  42. }
  43. return res;
  44. }
  45. }
  46. class SmartSelectController {
  47. constructor(_editor) {
  48. this._editor = _editor;
  49. this._ignoreSelection = false;
  50. }
  51. static get(editor) {
  52. return editor.getContribution(SmartSelectController.ID);
  53. }
  54. dispose() {
  55. var _a;
  56. (_a = this._selectionListener) === null || _a === void 0 ? void 0 : _a.dispose();
  57. }
  58. run(forward) {
  59. return __awaiter(this, void 0, void 0, function* () {
  60. if (!this._editor.hasModel()) {
  61. return;
  62. }
  63. const selections = this._editor.getSelections();
  64. const model = this._editor.getModel();
  65. if (!modes.SelectionRangeRegistry.has(model)) {
  66. return;
  67. }
  68. if (!this._state) {
  69. yield provideSelectionRanges(model, selections.map(s => s.getPosition()), this._editor.getOption(101 /* smartSelect */), CancellationToken.None).then(ranges => {
  70. var _a;
  71. if (!arrays.isNonEmptyArray(ranges) || ranges.length !== selections.length) {
  72. // invalid result
  73. return;
  74. }
  75. if (!this._editor.hasModel() || !arrays.equals(this._editor.getSelections(), selections, (a, b) => a.equalsSelection(b))) {
  76. // invalid editor state
  77. return;
  78. }
  79. for (let i = 0; i < ranges.length; i++) {
  80. ranges[i] = ranges[i].filter(range => {
  81. // filter ranges inside the selection
  82. return range.containsPosition(selections[i].getStartPosition()) && range.containsPosition(selections[i].getEndPosition());
  83. });
  84. // prepend current selection
  85. ranges[i].unshift(selections[i]);
  86. }
  87. this._state = ranges.map(ranges => new SelectionRanges(0, ranges));
  88. // listen to caret move and forget about state
  89. (_a = this._selectionListener) === null || _a === void 0 ? void 0 : _a.dispose();
  90. this._selectionListener = this._editor.onDidChangeCursorPosition(() => {
  91. var _a;
  92. if (!this._ignoreSelection) {
  93. (_a = this._selectionListener) === null || _a === void 0 ? void 0 : _a.dispose();
  94. this._state = undefined;
  95. }
  96. });
  97. });
  98. }
  99. if (!this._state) {
  100. // no state
  101. return;
  102. }
  103. this._state = this._state.map(state => state.mov(forward));
  104. const newSelections = this._state.map(state => Selection.fromPositions(state.ranges[state.index].getStartPosition(), state.ranges[state.index].getEndPosition()));
  105. this._ignoreSelection = true;
  106. try {
  107. this._editor.setSelections(newSelections);
  108. }
  109. finally {
  110. this._ignoreSelection = false;
  111. }
  112. });
  113. }
  114. }
  115. SmartSelectController.ID = 'editor.contrib.smartSelectController';
  116. class AbstractSmartSelect extends EditorAction {
  117. constructor(forward, opts) {
  118. super(opts);
  119. this._forward = forward;
  120. }
  121. run(_accessor, editor) {
  122. return __awaiter(this, void 0, void 0, function* () {
  123. let controller = SmartSelectController.get(editor);
  124. if (controller) {
  125. yield controller.run(this._forward);
  126. }
  127. });
  128. }
  129. }
  130. class GrowSelectionAction extends AbstractSmartSelect {
  131. constructor() {
  132. super(true, {
  133. id: 'editor.action.smartSelect.expand',
  134. label: nls.localize('smartSelect.expand', "Expand Selection"),
  135. alias: 'Expand Selection',
  136. precondition: undefined,
  137. kbOpts: {
  138. kbExpr: EditorContextKeys.editorTextFocus,
  139. primary: 1024 /* Shift */ | 512 /* Alt */ | 17 /* RightArrow */,
  140. mac: {
  141. primary: 2048 /* CtrlCmd */ | 256 /* WinCtrl */ | 1024 /* Shift */ | 17 /* RightArrow */,
  142. secondary: [256 /* WinCtrl */ | 1024 /* Shift */ | 17 /* RightArrow */],
  143. },
  144. weight: 100 /* EditorContrib */
  145. },
  146. menuOpts: {
  147. menuId: MenuId.MenubarSelectionMenu,
  148. group: '1_basic',
  149. title: nls.localize({ key: 'miSmartSelectGrow', comment: ['&& denotes a mnemonic'] }, "&&Expand Selection"),
  150. order: 2
  151. }
  152. });
  153. }
  154. }
  155. // renamed command id
  156. CommandsRegistry.registerCommandAlias('editor.action.smartSelect.grow', 'editor.action.smartSelect.expand');
  157. class ShrinkSelectionAction extends AbstractSmartSelect {
  158. constructor() {
  159. super(false, {
  160. id: 'editor.action.smartSelect.shrink',
  161. label: nls.localize('smartSelect.shrink', "Shrink Selection"),
  162. alias: 'Shrink Selection',
  163. precondition: undefined,
  164. kbOpts: {
  165. kbExpr: EditorContextKeys.editorTextFocus,
  166. primary: 1024 /* Shift */ | 512 /* Alt */ | 15 /* LeftArrow */,
  167. mac: {
  168. primary: 2048 /* CtrlCmd */ | 256 /* WinCtrl */ | 1024 /* Shift */ | 15 /* LeftArrow */,
  169. secondary: [256 /* WinCtrl */ | 1024 /* Shift */ | 15 /* LeftArrow */],
  170. },
  171. weight: 100 /* EditorContrib */
  172. },
  173. menuOpts: {
  174. menuId: MenuId.MenubarSelectionMenu,
  175. group: '1_basic',
  176. title: nls.localize({ key: 'miSmartSelectShrink', comment: ['&& denotes a mnemonic'] }, "&&Shrink Selection"),
  177. order: 3
  178. }
  179. });
  180. }
  181. }
  182. registerEditorContribution(SmartSelectController.ID, SmartSelectController);
  183. registerEditorAction(GrowSelectionAction);
  184. registerEditorAction(ShrinkSelectionAction);
  185. // word selection
  186. modes.SelectionRangeRegistry.register('*', new WordSelectionRangeProvider());
  187. export function provideSelectionRanges(model, positions, options, token) {
  188. return __awaiter(this, void 0, void 0, function* () {
  189. const providers = modes.SelectionRangeRegistry.all(model);
  190. if (providers.length === 1) {
  191. // add word selection and bracket selection when no provider exists
  192. providers.unshift(new BracketSelectionRangeProvider());
  193. }
  194. let work = [];
  195. let allRawRanges = [];
  196. for (const provider of providers) {
  197. work.push(Promise.resolve(provider.provideSelectionRanges(model, positions, token)).then(allProviderRanges => {
  198. if (arrays.isNonEmptyArray(allProviderRanges) && allProviderRanges.length === positions.length) {
  199. for (let i = 0; i < positions.length; i++) {
  200. if (!allRawRanges[i]) {
  201. allRawRanges[i] = [];
  202. }
  203. for (const oneProviderRanges of allProviderRanges[i]) {
  204. if (Range.isIRange(oneProviderRanges.range) && Range.containsPosition(oneProviderRanges.range, positions[i])) {
  205. allRawRanges[i].push(Range.lift(oneProviderRanges.range));
  206. }
  207. }
  208. }
  209. }
  210. }, onUnexpectedExternalError));
  211. }
  212. yield Promise.all(work);
  213. return allRawRanges.map(oneRawRanges => {
  214. if (oneRawRanges.length === 0) {
  215. return [];
  216. }
  217. // sort all by start/end position
  218. oneRawRanges.sort((a, b) => {
  219. if (Position.isBefore(a.getStartPosition(), b.getStartPosition())) {
  220. return 1;
  221. }
  222. else if (Position.isBefore(b.getStartPosition(), a.getStartPosition())) {
  223. return -1;
  224. }
  225. else if (Position.isBefore(a.getEndPosition(), b.getEndPosition())) {
  226. return -1;
  227. }
  228. else if (Position.isBefore(b.getEndPosition(), a.getEndPosition())) {
  229. return 1;
  230. }
  231. else {
  232. return 0;
  233. }
  234. });
  235. // remove ranges that don't contain the former range or that are equal to the
  236. // former range
  237. let oneRanges = [];
  238. let last;
  239. for (const range of oneRawRanges) {
  240. if (!last || (Range.containsRange(range, last) && !Range.equalsRange(range, last))) {
  241. oneRanges.push(range);
  242. last = range;
  243. }
  244. }
  245. if (!options.selectLeadingAndTrailingWhitespace) {
  246. return oneRanges;
  247. }
  248. // add ranges that expand trivia at line starts and ends whenever a range
  249. // wraps onto the a new line
  250. let oneRangesWithTrivia = [oneRanges[0]];
  251. for (let i = 1; i < oneRanges.length; i++) {
  252. const prev = oneRanges[i - 1];
  253. const cur = oneRanges[i];
  254. if (cur.startLineNumber !== prev.startLineNumber || cur.endLineNumber !== prev.endLineNumber) {
  255. // add line/block range without leading/failing whitespace
  256. const rangeNoWhitespace = new Range(prev.startLineNumber, model.getLineFirstNonWhitespaceColumn(prev.startLineNumber), prev.endLineNumber, model.getLineLastNonWhitespaceColumn(prev.endLineNumber));
  257. if (rangeNoWhitespace.containsRange(prev) && !rangeNoWhitespace.equalsRange(prev) && cur.containsRange(rangeNoWhitespace) && !cur.equalsRange(rangeNoWhitespace)) {
  258. oneRangesWithTrivia.push(rangeNoWhitespace);
  259. }
  260. // add line/block range
  261. const rangeFull = new Range(prev.startLineNumber, 1, prev.endLineNumber, model.getLineMaxColumn(prev.endLineNumber));
  262. if (rangeFull.containsRange(prev) && !rangeFull.equalsRange(rangeNoWhitespace) && cur.containsRange(rangeFull) && !cur.equalsRange(rangeFull)) {
  263. oneRangesWithTrivia.push(rangeFull);
  264. }
  265. }
  266. oneRangesWithTrivia.push(cur);
  267. }
  268. return oneRangesWithTrivia;
  269. });
  270. });
  271. }
  272. registerModelCommand('_executeSelectionRangeProvider', function (model, ...args) {
  273. const [positions] = args;
  274. return provideSelectionRanges(model, positions, { selectLeadingAndTrailingWhitespace: true }, CancellationToken.None);
  275. });