1990671afdec29dd36efbafafed7e4f0031e6a3220a9a82fa6e01b4ae1a70796a683bc927069c5b10489912c08d7a41a4d5448fe952806c36c87888a706d9a 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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 * as dom from '../../../../base/browser/dom.js';
  6. import { Sash } from '../../../../base/browser/ui/sash/sash.js';
  7. import { Color, RGBA } from '../../../../base/common/color.js';
  8. import { IdGenerator } from '../../../../base/common/idGenerator.js';
  9. import { DisposableStore } from '../../../../base/common/lifecycle.js';
  10. import * as objects from '../../../../base/common/objects.js';
  11. import './zoneWidget.css';
  12. import { Range } from '../../../common/core/range.js';
  13. import { ModelDecorationOptions } from '../../../common/model/textModel.js';
  14. const defaultColor = new Color(new RGBA(0, 122, 204));
  15. const defaultOptions = {
  16. showArrow: true,
  17. showFrame: true,
  18. className: '',
  19. frameColor: defaultColor,
  20. arrowColor: defaultColor,
  21. keepEditorSelection: false
  22. };
  23. const WIDGET_ID = 'vs.editor.contrib.zoneWidget';
  24. export class ViewZoneDelegate {
  25. constructor(domNode, afterLineNumber, afterColumn, heightInLines, onDomNodeTop, onComputedHeight) {
  26. this.id = ''; // A valid zone id should be greater than 0
  27. this.domNode = domNode;
  28. this.afterLineNumber = afterLineNumber;
  29. this.afterColumn = afterColumn;
  30. this.heightInLines = heightInLines;
  31. this._onDomNodeTop = onDomNodeTop;
  32. this._onComputedHeight = onComputedHeight;
  33. }
  34. onDomNodeTop(top) {
  35. this._onDomNodeTop(top);
  36. }
  37. onComputedHeight(height) {
  38. this._onComputedHeight(height);
  39. }
  40. }
  41. export class OverlayWidgetDelegate {
  42. constructor(id, domNode) {
  43. this._id = id;
  44. this._domNode = domNode;
  45. }
  46. getId() {
  47. return this._id;
  48. }
  49. getDomNode() {
  50. return this._domNode;
  51. }
  52. getPosition() {
  53. return null;
  54. }
  55. }
  56. class Arrow {
  57. constructor(_editor) {
  58. this._editor = _editor;
  59. this._ruleName = Arrow._IdGenerator.nextId();
  60. this._decorations = this._editor.createDecorationsCollection();
  61. this._color = null;
  62. this._height = -1;
  63. }
  64. dispose() {
  65. this.hide();
  66. dom.removeCSSRulesContainingSelector(this._ruleName);
  67. }
  68. set color(value) {
  69. if (this._color !== value) {
  70. this._color = value;
  71. this._updateStyle();
  72. }
  73. }
  74. set height(value) {
  75. if (this._height !== value) {
  76. this._height = value;
  77. this._updateStyle();
  78. }
  79. }
  80. _updateStyle() {
  81. dom.removeCSSRulesContainingSelector(this._ruleName);
  82. dom.createCSSRule(`.monaco-editor ${this._ruleName}`, `border-style: solid; border-color: transparent; border-bottom-color: ${this._color}; border-width: ${this._height}px; bottom: -${this._height}px; margin-left: -${this._height}px; `);
  83. }
  84. show(where) {
  85. if (where.column === 1) {
  86. // the arrow isn't pretty at column 1 and we need to push it out a little
  87. where = { lineNumber: where.lineNumber, column: 2 };
  88. }
  89. this._decorations.set([{
  90. range: Range.fromPositions(where),
  91. options: {
  92. description: 'zone-widget-arrow',
  93. className: this._ruleName,
  94. stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */
  95. }
  96. }]);
  97. }
  98. hide() {
  99. this._decorations.clear();
  100. }
  101. }
  102. Arrow._IdGenerator = new IdGenerator('.arrow-decoration-');
  103. export class ZoneWidget {
  104. constructor(editor, options = {}) {
  105. this._arrow = null;
  106. this._overlayWidget = null;
  107. this._resizeSash = null;
  108. this._viewZone = null;
  109. this._disposables = new DisposableStore();
  110. this.container = null;
  111. this._isShowing = false;
  112. this.editor = editor;
  113. this._positionMarkerId = this.editor.createDecorationsCollection();
  114. this.options = objects.deepClone(options);
  115. objects.mixin(this.options, defaultOptions, false);
  116. this.domNode = document.createElement('div');
  117. if (!this.options.isAccessible) {
  118. this.domNode.setAttribute('aria-hidden', 'true');
  119. this.domNode.setAttribute('role', 'presentation');
  120. }
  121. this._disposables.add(this.editor.onDidLayoutChange((info) => {
  122. const width = this._getWidth(info);
  123. this.domNode.style.width = width + 'px';
  124. this.domNode.style.left = this._getLeft(info) + 'px';
  125. this._onWidth(width);
  126. }));
  127. }
  128. dispose() {
  129. if (this._overlayWidget) {
  130. this.editor.removeOverlayWidget(this._overlayWidget);
  131. this._overlayWidget = null;
  132. }
  133. if (this._viewZone) {
  134. this.editor.changeViewZones(accessor => {
  135. if (this._viewZone) {
  136. accessor.removeZone(this._viewZone.id);
  137. }
  138. this._viewZone = null;
  139. });
  140. }
  141. this._positionMarkerId.clear();
  142. this._disposables.dispose();
  143. }
  144. create() {
  145. this.domNode.classList.add('zone-widget');
  146. if (this.options.className) {
  147. this.domNode.classList.add(this.options.className);
  148. }
  149. this.container = document.createElement('div');
  150. this.container.classList.add('zone-widget-container');
  151. this.domNode.appendChild(this.container);
  152. if (this.options.showArrow) {
  153. this._arrow = new Arrow(this.editor);
  154. this._disposables.add(this._arrow);
  155. }
  156. this._fillContainer(this.container);
  157. this._initSash();
  158. this._applyStyles();
  159. }
  160. style(styles) {
  161. if (styles.frameColor) {
  162. this.options.frameColor = styles.frameColor;
  163. }
  164. if (styles.arrowColor) {
  165. this.options.arrowColor = styles.arrowColor;
  166. }
  167. this._applyStyles();
  168. }
  169. _applyStyles() {
  170. if (this.container && this.options.frameColor) {
  171. const frameColor = this.options.frameColor.toString();
  172. this.container.style.borderTopColor = frameColor;
  173. this.container.style.borderBottomColor = frameColor;
  174. }
  175. if (this._arrow && this.options.arrowColor) {
  176. const arrowColor = this.options.arrowColor.toString();
  177. this._arrow.color = arrowColor;
  178. }
  179. }
  180. _getWidth(info) {
  181. return info.width - info.minimap.minimapWidth - info.verticalScrollbarWidth;
  182. }
  183. _getLeft(info) {
  184. // If minimap is to the left, we move beyond it
  185. if (info.minimap.minimapWidth > 0 && info.minimap.minimapLeft === 0) {
  186. return info.minimap.minimapWidth;
  187. }
  188. return 0;
  189. }
  190. _onViewZoneTop(top) {
  191. this.domNode.style.top = top + 'px';
  192. }
  193. _onViewZoneHeight(height) {
  194. this.domNode.style.height = `${height}px`;
  195. if (this.container) {
  196. const containerHeight = height - this._decoratingElementsHeight();
  197. this.container.style.height = `${containerHeight}px`;
  198. const layoutInfo = this.editor.getLayoutInfo();
  199. this._doLayout(containerHeight, this._getWidth(layoutInfo));
  200. }
  201. if (this._resizeSash) {
  202. this._resizeSash.layout();
  203. }
  204. }
  205. get position() {
  206. const range = this._positionMarkerId.getRange(0);
  207. if (!range) {
  208. return undefined;
  209. }
  210. return range.getStartPosition();
  211. }
  212. show(rangeOrPos, heightInLines) {
  213. const range = Range.isIRange(rangeOrPos) ? Range.lift(rangeOrPos) : Range.fromPositions(rangeOrPos);
  214. this._isShowing = true;
  215. this._showImpl(range, heightInLines);
  216. this._isShowing = false;
  217. this._positionMarkerId.set([{ range, options: ModelDecorationOptions.EMPTY }]);
  218. }
  219. hide() {
  220. if (this._viewZone) {
  221. this.editor.changeViewZones(accessor => {
  222. if (this._viewZone) {
  223. accessor.removeZone(this._viewZone.id);
  224. }
  225. });
  226. this._viewZone = null;
  227. }
  228. if (this._overlayWidget) {
  229. this.editor.removeOverlayWidget(this._overlayWidget);
  230. this._overlayWidget = null;
  231. }
  232. if (this._arrow) {
  233. this._arrow.hide();
  234. }
  235. }
  236. _decoratingElementsHeight() {
  237. const lineHeight = this.editor.getOption(61 /* EditorOption.lineHeight */);
  238. let result = 0;
  239. if (this.options.showArrow) {
  240. const arrowHeight = Math.round(lineHeight / 3);
  241. result += 2 * arrowHeight;
  242. }
  243. if (this.options.showFrame) {
  244. const frameThickness = Math.round(lineHeight / 9);
  245. result += 2 * frameThickness;
  246. }
  247. return result;
  248. }
  249. _showImpl(where, heightInLines) {
  250. const position = where.getStartPosition();
  251. const layoutInfo = this.editor.getLayoutInfo();
  252. const width = this._getWidth(layoutInfo);
  253. this.domNode.style.width = `${width}px`;
  254. this.domNode.style.left = this._getLeft(layoutInfo) + 'px';
  255. // Render the widget as zone (rendering) and widget (lifecycle)
  256. const viewZoneDomNode = document.createElement('div');
  257. viewZoneDomNode.style.overflow = 'hidden';
  258. const lineHeight = this.editor.getOption(61 /* EditorOption.lineHeight */);
  259. // adjust heightInLines to viewport
  260. const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8);
  261. heightInLines = Math.min(heightInLines, maxHeightInLines);
  262. let arrowHeight = 0;
  263. let frameThickness = 0;
  264. // Render the arrow one 1/3 of an editor line height
  265. if (this._arrow && this.options.showArrow) {
  266. arrowHeight = Math.round(lineHeight / 3);
  267. this._arrow.height = arrowHeight;
  268. this._arrow.show(position);
  269. }
  270. // Render the frame as 1/9 of an editor line height
  271. if (this.options.showFrame) {
  272. frameThickness = Math.round(lineHeight / 9);
  273. }
  274. // insert zone widget
  275. this.editor.changeViewZones((accessor) => {
  276. if (this._viewZone) {
  277. accessor.removeZone(this._viewZone.id);
  278. }
  279. if (this._overlayWidget) {
  280. this.editor.removeOverlayWidget(this._overlayWidget);
  281. this._overlayWidget = null;
  282. }
  283. this.domNode.style.top = '-1000px';
  284. this._viewZone = new ViewZoneDelegate(viewZoneDomNode, position.lineNumber, position.column, heightInLines, (top) => this._onViewZoneTop(top), (height) => this._onViewZoneHeight(height));
  285. this._viewZone.id = accessor.addZone(this._viewZone);
  286. this._overlayWidget = new OverlayWidgetDelegate(WIDGET_ID + this._viewZone.id, this.domNode);
  287. this.editor.addOverlayWidget(this._overlayWidget);
  288. });
  289. if (this.container && this.options.showFrame) {
  290. const width = this.options.frameWidth ? this.options.frameWidth : frameThickness;
  291. this.container.style.borderTopWidth = width + 'px';
  292. this.container.style.borderBottomWidth = width + 'px';
  293. }
  294. const containerHeight = heightInLines * lineHeight - this._decoratingElementsHeight();
  295. if (this.container) {
  296. this.container.style.top = arrowHeight + 'px';
  297. this.container.style.height = containerHeight + 'px';
  298. this.container.style.overflow = 'hidden';
  299. }
  300. this._doLayout(containerHeight, width);
  301. if (!this.options.keepEditorSelection) {
  302. this.editor.setSelection(where);
  303. }
  304. const model = this.editor.getModel();
  305. if (model) {
  306. const revealLine = where.endLineNumber + 1;
  307. if (revealLine <= model.getLineCount()) {
  308. // reveal line below the zone widget
  309. this.revealLine(revealLine, false);
  310. }
  311. else {
  312. // reveal last line atop
  313. this.revealLine(model.getLineCount(), true);
  314. }
  315. }
  316. }
  317. revealLine(lineNumber, isLastLine) {
  318. if (isLastLine) {
  319. this.editor.revealLineInCenter(lineNumber, 0 /* ScrollType.Smooth */);
  320. }
  321. else {
  322. this.editor.revealLine(lineNumber, 0 /* ScrollType.Smooth */);
  323. }
  324. }
  325. setCssClass(className, classToReplace) {
  326. if (!this.container) {
  327. return;
  328. }
  329. if (classToReplace) {
  330. this.container.classList.remove(classToReplace);
  331. }
  332. this.container.classList.add(className);
  333. }
  334. _onWidth(widthInPixel) {
  335. // implement in subclass
  336. }
  337. _doLayout(heightInPixel, widthInPixel) {
  338. // implement in subclass
  339. }
  340. _relayout(newHeightInLines) {
  341. if (this._viewZone && this._viewZone.heightInLines !== newHeightInLines) {
  342. this.editor.changeViewZones(accessor => {
  343. if (this._viewZone) {
  344. this._viewZone.heightInLines = newHeightInLines;
  345. accessor.layoutZone(this._viewZone.id);
  346. }
  347. });
  348. }
  349. }
  350. // --- sash
  351. _initSash() {
  352. if (this._resizeSash) {
  353. return;
  354. }
  355. this._resizeSash = this._disposables.add(new Sash(this.domNode, this, { orientation: 1 /* Orientation.HORIZONTAL */ }));
  356. if (!this.options.isResizeable) {
  357. this._resizeSash.state = 0 /* SashState.Disabled */;
  358. }
  359. let data;
  360. this._disposables.add(this._resizeSash.onDidStart((e) => {
  361. if (this._viewZone) {
  362. data = {
  363. startY: e.startY,
  364. heightInLines: this._viewZone.heightInLines,
  365. };
  366. }
  367. }));
  368. this._disposables.add(this._resizeSash.onDidEnd(() => {
  369. data = undefined;
  370. }));
  371. this._disposables.add(this._resizeSash.onDidChange((evt) => {
  372. if (data) {
  373. const lineDelta = (evt.currentY - data.startY) / this.editor.getOption(61 /* EditorOption.lineHeight */);
  374. const roundedLineDelta = lineDelta < 0 ? Math.ceil(lineDelta) : Math.floor(lineDelta);
  375. const newHeightInLines = data.heightInLines + roundedLineDelta;
  376. if (newHeightInLines > 5 && newHeightInLines < 35) {
  377. this._relayout(newHeightInLines);
  378. }
  379. }
  380. }));
  381. }
  382. getHorizontalSashLeft() {
  383. return 0;
  384. }
  385. getHorizontalSashTop() {
  386. return (this.domNode.style.height === null ? 0 : parseInt(this.domNode.style.height)) - (this._decoratingElementsHeight() / 2);
  387. }
  388. getHorizontalSashWidth() {
  389. const layoutInfo = this.editor.getLayoutInfo();
  390. return layoutInfo.width - layoutInfo.minimap.minimapWidth;
  391. }
  392. }