00333f87d0171461d6b71f46b337159d0daa503b39e00a1edd871920333675c33b81b9182334a917b0571b1e9c40ee819207f433b107d6a5d5c8063b0fb586 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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. import { RunOnceScheduler } from '../../../../base/common/async.js';
  6. import { Disposable } from '../../../../base/common/lifecycle.js';
  7. import './bracketMatching.css';
  8. import { EditorAction, registerEditorAction, registerEditorContribution } from '../../../browser/editorExtensions.js';
  9. import { Position } from '../../../common/core/position.js';
  10. import { Range } from '../../../common/core/range.js';
  11. import { Selection } from '../../../common/core/selection.js';
  12. import { EditorContextKeys } from '../../../common/editorContextKeys.js';
  13. import { OverviewRulerLane } from '../../../common/model.js';
  14. import { ModelDecorationOptions } from '../../../common/model/textModel.js';
  15. import { editorBracketMatchBackground, editorBracketMatchBorder } from '../../../common/core/editorColorRegistry.js';
  16. import * as nls from '../../../../nls.js';
  17. import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js';
  18. import { registerColor } from '../../../../platform/theme/common/colorRegistry.js';
  19. import { registerThemingParticipant, themeColorFromId } from '../../../../platform/theme/common/themeService.js';
  20. const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', { dark: '#A0A0A0', light: '#A0A0A0', hcDark: '#A0A0A0', hcLight: '#A0A0A0' }, nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.'));
  21. class JumpToBracketAction extends EditorAction {
  22. constructor() {
  23. super({
  24. id: 'editor.action.jumpToBracket',
  25. label: nls.localize('smartSelect.jumpBracket', "Go to Bracket"),
  26. alias: 'Go to Bracket',
  27. precondition: undefined,
  28. kbOpts: {
  29. kbExpr: EditorContextKeys.editorTextFocus,
  30. primary: 2048 /* KeyMod.CtrlCmd */ | 1024 /* KeyMod.Shift */ | 88 /* KeyCode.Backslash */,
  31. weight: 100 /* KeybindingWeight.EditorContrib */
  32. }
  33. });
  34. }
  35. run(accessor, editor) {
  36. var _a;
  37. (_a = BracketMatchingController.get(editor)) === null || _a === void 0 ? void 0 : _a.jumpToBracket();
  38. }
  39. }
  40. class SelectToBracketAction extends EditorAction {
  41. constructor() {
  42. super({
  43. id: 'editor.action.selectToBracket',
  44. label: nls.localize('smartSelect.selectToBracket', "Select to Bracket"),
  45. alias: 'Select to Bracket',
  46. precondition: undefined,
  47. description: {
  48. description: `Select to Bracket`,
  49. args: [{
  50. name: 'args',
  51. schema: {
  52. type: 'object',
  53. properties: {
  54. 'selectBrackets': {
  55. type: 'boolean',
  56. default: true
  57. }
  58. },
  59. }
  60. }]
  61. }
  62. });
  63. }
  64. run(accessor, editor, args) {
  65. var _a;
  66. let selectBrackets = true;
  67. if (args && args.selectBrackets === false) {
  68. selectBrackets = false;
  69. }
  70. (_a = BracketMatchingController.get(editor)) === null || _a === void 0 ? void 0 : _a.selectToBracket(selectBrackets);
  71. }
  72. }
  73. class BracketsData {
  74. constructor(position, brackets, options) {
  75. this.position = position;
  76. this.brackets = brackets;
  77. this.options = options;
  78. }
  79. }
  80. export class BracketMatchingController extends Disposable {
  81. constructor(editor) {
  82. super();
  83. this._editor = editor;
  84. this._lastBracketsData = [];
  85. this._lastVersionId = 0;
  86. this._decorations = this._editor.createDecorationsCollection();
  87. this._updateBracketsSoon = this._register(new RunOnceScheduler(() => this._updateBrackets(), 50));
  88. this._matchBrackets = this._editor.getOption(66 /* EditorOption.matchBrackets */);
  89. this._updateBracketsSoon.schedule();
  90. this._register(editor.onDidChangeCursorPosition((e) => {
  91. if (this._matchBrackets === 'never') {
  92. // Early exit if nothing needs to be done!
  93. // Leave some form of early exit check here if you wish to continue being a cursor position change listener ;)
  94. return;
  95. }
  96. this._updateBracketsSoon.schedule();
  97. }));
  98. this._register(editor.onDidChangeModelContent((e) => {
  99. this._updateBracketsSoon.schedule();
  100. }));
  101. this._register(editor.onDidChangeModel((e) => {
  102. this._lastBracketsData = [];
  103. this._updateBracketsSoon.schedule();
  104. }));
  105. this._register(editor.onDidChangeModelLanguageConfiguration((e) => {
  106. this._lastBracketsData = [];
  107. this._updateBracketsSoon.schedule();
  108. }));
  109. this._register(editor.onDidChangeConfiguration((e) => {
  110. if (e.hasChanged(66 /* EditorOption.matchBrackets */)) {
  111. this._matchBrackets = this._editor.getOption(66 /* EditorOption.matchBrackets */);
  112. this._decorations.clear();
  113. this._lastBracketsData = [];
  114. this._lastVersionId = 0;
  115. this._updateBracketsSoon.schedule();
  116. }
  117. }));
  118. this._register(editor.onDidBlurEditorWidget(() => {
  119. this._updateBracketsSoon.schedule();
  120. }));
  121. this._register(editor.onDidFocusEditorWidget(() => {
  122. this._updateBracketsSoon.schedule();
  123. }));
  124. }
  125. static get(editor) {
  126. return editor.getContribution(BracketMatchingController.ID);
  127. }
  128. jumpToBracket() {
  129. if (!this._editor.hasModel()) {
  130. return;
  131. }
  132. const model = this._editor.getModel();
  133. const newSelections = this._editor.getSelections().map(selection => {
  134. const position = selection.getStartPosition();
  135. // find matching brackets if position is on a bracket
  136. const brackets = model.bracketPairs.matchBracket(position);
  137. let newCursorPosition = null;
  138. if (brackets) {
  139. if (brackets[0].containsPosition(position) && !brackets[1].containsPosition(position)) {
  140. newCursorPosition = brackets[1].getStartPosition();
  141. }
  142. else if (brackets[1].containsPosition(position)) {
  143. newCursorPosition = brackets[0].getStartPosition();
  144. }
  145. }
  146. else {
  147. // find the enclosing brackets if the position isn't on a matching bracket
  148. const enclosingBrackets = model.bracketPairs.findEnclosingBrackets(position);
  149. if (enclosingBrackets) {
  150. newCursorPosition = enclosingBrackets[1].getStartPosition();
  151. }
  152. else {
  153. // no enclosing brackets, try the very first next bracket
  154. const nextBracket = model.bracketPairs.findNextBracket(position);
  155. if (nextBracket && nextBracket.range) {
  156. newCursorPosition = nextBracket.range.getStartPosition();
  157. }
  158. }
  159. }
  160. if (newCursorPosition) {
  161. return new Selection(newCursorPosition.lineNumber, newCursorPosition.column, newCursorPosition.lineNumber, newCursorPosition.column);
  162. }
  163. return new Selection(position.lineNumber, position.column, position.lineNumber, position.column);
  164. });
  165. this._editor.setSelections(newSelections);
  166. this._editor.revealRange(newSelections[0]);
  167. }
  168. selectToBracket(selectBrackets) {
  169. if (!this._editor.hasModel()) {
  170. return;
  171. }
  172. const model = this._editor.getModel();
  173. const newSelections = [];
  174. this._editor.getSelections().forEach(selection => {
  175. const position = selection.getStartPosition();
  176. let brackets = model.bracketPairs.matchBracket(position);
  177. if (!brackets) {
  178. brackets = model.bracketPairs.findEnclosingBrackets(position);
  179. if (!brackets) {
  180. const nextBracket = model.bracketPairs.findNextBracket(position);
  181. if (nextBracket && nextBracket.range) {
  182. brackets = model.bracketPairs.matchBracket(nextBracket.range.getStartPosition());
  183. }
  184. }
  185. }
  186. let selectFrom = null;
  187. let selectTo = null;
  188. if (brackets) {
  189. brackets.sort(Range.compareRangesUsingStarts);
  190. const [open, close] = brackets;
  191. selectFrom = selectBrackets ? open.getStartPosition() : open.getEndPosition();
  192. selectTo = selectBrackets ? close.getEndPosition() : close.getStartPosition();
  193. if (close.containsPosition(position)) {
  194. // select backwards if the cursor was on the closing bracket
  195. const tmp = selectFrom;
  196. selectFrom = selectTo;
  197. selectTo = tmp;
  198. }
  199. }
  200. if (selectFrom && selectTo) {
  201. newSelections.push(new Selection(selectFrom.lineNumber, selectFrom.column, selectTo.lineNumber, selectTo.column));
  202. }
  203. });
  204. if (newSelections.length > 0) {
  205. this._editor.setSelections(newSelections);
  206. this._editor.revealRange(newSelections[0]);
  207. }
  208. }
  209. _updateBrackets() {
  210. if (this._matchBrackets === 'never') {
  211. return;
  212. }
  213. this._recomputeBrackets();
  214. const newDecorations = [];
  215. let newDecorationsLen = 0;
  216. for (const bracketData of this._lastBracketsData) {
  217. const brackets = bracketData.brackets;
  218. if (brackets) {
  219. newDecorations[newDecorationsLen++] = { range: brackets[0], options: bracketData.options };
  220. newDecorations[newDecorationsLen++] = { range: brackets[1], options: bracketData.options };
  221. }
  222. }
  223. this._decorations.set(newDecorations);
  224. }
  225. _recomputeBrackets() {
  226. if (!this._editor.hasModel() || !this._editor.hasWidgetFocus()) {
  227. // no model or no focus => no brackets!
  228. this._lastBracketsData = [];
  229. this._lastVersionId = 0;
  230. return;
  231. }
  232. const selections = this._editor.getSelections();
  233. if (selections.length > 100) {
  234. // no bracket matching for high numbers of selections
  235. this._lastBracketsData = [];
  236. this._lastVersionId = 0;
  237. return;
  238. }
  239. const model = this._editor.getModel();
  240. const versionId = model.getVersionId();
  241. let previousData = [];
  242. if (this._lastVersionId === versionId) {
  243. // use the previous data only if the model is at the same version id
  244. previousData = this._lastBracketsData;
  245. }
  246. const positions = [];
  247. let positionsLen = 0;
  248. for (let i = 0, len = selections.length; i < len; i++) {
  249. const selection = selections[i];
  250. if (selection.isEmpty()) {
  251. // will bracket match a cursor only if the selection is collapsed
  252. positions[positionsLen++] = selection.getStartPosition();
  253. }
  254. }
  255. // sort positions for `previousData` cache hits
  256. if (positions.length > 1) {
  257. positions.sort(Position.compare);
  258. }
  259. const newData = [];
  260. let newDataLen = 0;
  261. let previousIndex = 0;
  262. const previousLen = previousData.length;
  263. for (let i = 0, len = positions.length; i < len; i++) {
  264. const position = positions[i];
  265. while (previousIndex < previousLen && previousData[previousIndex].position.isBefore(position)) {
  266. previousIndex++;
  267. }
  268. if (previousIndex < previousLen && previousData[previousIndex].position.equals(position)) {
  269. newData[newDataLen++] = previousData[previousIndex];
  270. }
  271. else {
  272. let brackets = model.bracketPairs.matchBracket(position, 20 /* give at most 20ms to compute */);
  273. let options = BracketMatchingController._DECORATION_OPTIONS_WITH_OVERVIEW_RULER;
  274. if (!brackets && this._matchBrackets === 'always') {
  275. brackets = model.bracketPairs.findEnclosingBrackets(position, 20 /* give at most 20ms to compute */);
  276. options = BracketMatchingController._DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER;
  277. }
  278. newData[newDataLen++] = new BracketsData(position, brackets, options);
  279. }
  280. }
  281. this._lastBracketsData = newData;
  282. this._lastVersionId = versionId;
  283. }
  284. }
  285. BracketMatchingController.ID = 'editor.contrib.bracketMatchingController';
  286. BracketMatchingController._DECORATION_OPTIONS_WITH_OVERVIEW_RULER = ModelDecorationOptions.register({
  287. description: 'bracket-match-overview',
  288. stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */,
  289. className: 'bracket-match',
  290. overviewRuler: {
  291. color: themeColorFromId(overviewRulerBracketMatchForeground),
  292. position: OverviewRulerLane.Center
  293. }
  294. });
  295. BracketMatchingController._DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER = ModelDecorationOptions.register({
  296. description: 'bracket-match-no-overview',
  297. stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */,
  298. className: 'bracket-match'
  299. });
  300. registerEditorContribution(BracketMatchingController.ID, BracketMatchingController);
  301. registerEditorAction(SelectToBracketAction);
  302. registerEditorAction(JumpToBracketAction);
  303. registerThemingParticipant((theme, collector) => {
  304. const bracketMatchBackground = theme.getColor(editorBracketMatchBackground);
  305. if (bracketMatchBackground) {
  306. collector.addRule(`.monaco-editor .bracket-match { background-color: ${bracketMatchBackground}; }`);
  307. }
  308. const bracketMatchBorder = theme.getColor(editorBracketMatchBorder);
  309. if (bracketMatchBorder) {
  310. collector.addRule(`.monaco-editor .bracket-match { border: 1px solid ${bracketMatchBorder}; }`);
  311. }
  312. });
  313. // Go to menu
  314. MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
  315. group: '5_infile_nav',
  316. command: {
  317. id: 'editor.action.jumpToBracket',
  318. title: nls.localize({ key: 'miGoToBracket', comment: ['&& denotes a mnemonic'] }, "Go to &&Bracket")
  319. },
  320. order: 2
  321. });