bracketMatching.js 14 KB

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