ghostTextWidget.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  6. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  7. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  8. else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  9. return c > 3 && r && Object.defineProperty(target, key, r), r;
  10. };
  11. var __param = (this && this.__param) || function (paramIndex, decorator) {
  12. return function (target, key) { decorator(target, key, paramIndex); }
  13. };
  14. var _a;
  15. import * as dom from '../../../base/browser/dom.js';
  16. import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
  17. import * as strings from '../../../base/common/strings.js';
  18. import './ghostText.css';
  19. import { Configuration } from '../../browser/config/configuration.js';
  20. import { EditorFontLigatures } from '../../common/config/editorOptions.js';
  21. import { LineTokens } from '../../common/core/lineTokens.js';
  22. import { Position } from '../../common/core/position.js';
  23. import { Range } from '../../common/core/range.js';
  24. import { createStringBuilder } from '../../common/core/stringBuilder.js';
  25. import { IModeService } from '../../common/services/modeService.js';
  26. import { ghostTextBackground, ghostTextBorder, ghostTextForeground } from '../../common/view/editorColorRegistry.js';
  27. import { LineDecoration } from '../../common/viewLayout/lineDecorations.js';
  28. import { RenderLineInput, renderViewLine } from '../../common/viewLayout/viewLineRenderer.js';
  29. import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
  30. import { registerThemingParticipant } from '../../../platform/theme/common/themeService.js';
  31. const ttPolicy = (_a = window.trustedTypes) === null || _a === void 0 ? void 0 : _a.createPolicy('editorGhostText', { createHTML: value => value });
  32. let GhostTextWidget = class GhostTextWidget extends Disposable {
  33. constructor(editor, model, instantiationService, modeService) {
  34. super();
  35. this.editor = editor;
  36. this.model = model;
  37. this.instantiationService = instantiationService;
  38. this.modeService = modeService;
  39. this.disposed = false;
  40. this.partsWidget = this._register(this.instantiationService.createInstance(DecorationsWidget, this.editor));
  41. this.additionalLinesWidget = this._register(new AdditionalLinesWidget(this.editor, this.modeService.languageIdCodec));
  42. this.viewMoreContentWidget = undefined;
  43. this._register(this.editor.onDidChangeConfiguration((e) => {
  44. if (e.hasChanged(29 /* disableMonospaceOptimizations */)
  45. || e.hasChanged(104 /* stopRenderingLineAfter */)
  46. || e.hasChanged(87 /* renderWhitespace */)
  47. || e.hasChanged(82 /* renderControlCharacters */)
  48. || e.hasChanged(44 /* fontLigatures */)
  49. || e.hasChanged(43 /* fontInfo */)
  50. || e.hasChanged(58 /* lineHeight */)) {
  51. this.update();
  52. }
  53. }));
  54. this._register(toDisposable(() => {
  55. var _a;
  56. this.disposed = true;
  57. this.update();
  58. (_a = this.viewMoreContentWidget) === null || _a === void 0 ? void 0 : _a.dispose();
  59. this.viewMoreContentWidget = undefined;
  60. }));
  61. this._register(model.onDidChange(() => {
  62. this.update();
  63. }));
  64. this.update();
  65. }
  66. shouldShowHoverAtViewZone(viewZoneId) {
  67. return (this.additionalLinesWidget.viewZoneId === viewZoneId);
  68. }
  69. update() {
  70. var _a;
  71. const ghostText = this.model.ghostText;
  72. if (!this.editor.hasModel() || !ghostText || this.disposed) {
  73. this.partsWidget.clear();
  74. this.additionalLinesWidget.clear();
  75. return;
  76. }
  77. const inlineTexts = new Array();
  78. const additionalLines = new Array();
  79. function addToAdditionalLines(lines, className) {
  80. if (additionalLines.length > 0) {
  81. const lastLine = additionalLines[additionalLines.length - 1];
  82. if (className) {
  83. lastLine.decorations.push(new LineDecoration(lastLine.content.length + 1, lastLine.content.length + 1 + lines[0].length, className, 0 /* Regular */));
  84. }
  85. lastLine.content += lines[0];
  86. lines = lines.slice(1);
  87. }
  88. for (const line of lines) {
  89. additionalLines.push({
  90. content: line,
  91. decorations: className ? [new LineDecoration(1, line.length + 1, className, 0 /* Regular */)] : []
  92. });
  93. }
  94. }
  95. const textBufferLine = this.editor.getModel().getLineContent(ghostText.lineNumber);
  96. this.editor.getModel().getLineTokens(ghostText.lineNumber);
  97. let hiddenTextStartColumn = undefined;
  98. let lastIdx = 0;
  99. for (const part of ghostText.parts) {
  100. let lines = part.lines;
  101. if (hiddenTextStartColumn === undefined) {
  102. inlineTexts.push({
  103. column: part.column,
  104. text: lines[0],
  105. preview: part.preview,
  106. });
  107. lines = lines.slice(1);
  108. }
  109. else {
  110. addToAdditionalLines([textBufferLine.substring(lastIdx, part.column - 1)], undefined);
  111. }
  112. if (lines.length > 0) {
  113. addToAdditionalLines(lines, 'ghost-text');
  114. if (hiddenTextStartColumn === undefined && part.column <= textBufferLine.length) {
  115. hiddenTextStartColumn = part.column;
  116. }
  117. }
  118. lastIdx = part.column - 1;
  119. }
  120. if (hiddenTextStartColumn !== undefined) {
  121. addToAdditionalLines([textBufferLine.substring(lastIdx)], undefined);
  122. }
  123. this.partsWidget.setParts(ghostText.lineNumber, inlineTexts, hiddenTextStartColumn !== undefined ? { column: hiddenTextStartColumn, length: textBufferLine.length + 1 - hiddenTextStartColumn } : undefined);
  124. this.additionalLinesWidget.updateLines(ghostText.lineNumber, additionalLines, ghostText.additionalReservedLineCount);
  125. if (ghostText.parts.some(p => p.lines.length < 0)) {
  126. // Not supported at the moment, condition is always false.
  127. this.viewMoreContentWidget = this.renderViewMoreLines(new Position(ghostText.lineNumber, this.editor.getModel().getLineMaxColumn(ghostText.lineNumber)), '', 0);
  128. }
  129. else {
  130. (_a = this.viewMoreContentWidget) === null || _a === void 0 ? void 0 : _a.dispose();
  131. this.viewMoreContentWidget = undefined;
  132. }
  133. }
  134. renderViewMoreLines(position, firstLineText, remainingLinesLength) {
  135. const fontInfo = this.editor.getOption(43 /* fontInfo */);
  136. const domNode = document.createElement('div');
  137. domNode.className = 'suggest-preview-additional-widget';
  138. Configuration.applyFontInfoSlow(domNode, fontInfo);
  139. const spacer = document.createElement('span');
  140. spacer.className = 'content-spacer';
  141. spacer.append(firstLineText);
  142. domNode.append(spacer);
  143. const newline = document.createElement('span');
  144. newline.className = 'content-newline suggest-preview-text';
  145. newline.append('⏎ ');
  146. domNode.append(newline);
  147. const disposableStore = new DisposableStore();
  148. const button = document.createElement('div');
  149. button.className = 'button suggest-preview-text';
  150. button.append(`+${remainingLinesLength} lines…`);
  151. disposableStore.add(dom.addStandardDisposableListener(button, 'mousedown', (e) => {
  152. var _a;
  153. (_a = this.model) === null || _a === void 0 ? void 0 : _a.setExpanded(true);
  154. e.preventDefault();
  155. this.editor.focus();
  156. }));
  157. domNode.append(button);
  158. return new ViewMoreLinesContentWidget(this.editor, position, domNode, disposableStore);
  159. }
  160. };
  161. GhostTextWidget = __decorate([
  162. __param(2, IInstantiationService),
  163. __param(3, IModeService)
  164. ], GhostTextWidget);
  165. export { GhostTextWidget };
  166. class DecorationsWidget {
  167. constructor(editor) {
  168. this.editor = editor;
  169. this.decorationIds = [];
  170. this.disposableStore = new DisposableStore();
  171. }
  172. dispose() {
  173. this.clear();
  174. this.disposableStore.dispose();
  175. }
  176. clear() {
  177. this.editor.deltaDecorations(this.decorationIds, []);
  178. this.disposableStore.clear();
  179. }
  180. setParts(lineNumber, parts, hiddenText) {
  181. this.disposableStore.clear();
  182. const textModel = this.editor.getModel();
  183. if (!textModel) {
  184. return;
  185. }
  186. const hiddenTextDecorations = new Array();
  187. if (hiddenText) {
  188. hiddenTextDecorations.push({
  189. range: Range.fromPositions(new Position(lineNumber, hiddenText.column), new Position(lineNumber, hiddenText.column + hiddenText.length)),
  190. options: {
  191. inlineClassName: 'ghost-text-hidden',
  192. description: 'ghost-text-hidden'
  193. }
  194. });
  195. }
  196. this.decorationIds = this.editor.deltaDecorations(this.decorationIds, parts.map(p => {
  197. return ({
  198. range: Range.fromPositions(new Position(lineNumber, p.column)),
  199. options: {
  200. description: 'ghost-text',
  201. after: { content: p.text, inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration' },
  202. showIfCollapsed: true,
  203. }
  204. });
  205. }).concat(hiddenTextDecorations));
  206. }
  207. }
  208. class AdditionalLinesWidget {
  209. constructor(editor, languageIdCodec) {
  210. this.editor = editor;
  211. this.languageIdCodec = languageIdCodec;
  212. this._viewZoneId = undefined;
  213. }
  214. get viewZoneId() { return this._viewZoneId; }
  215. dispose() {
  216. this.clear();
  217. }
  218. clear() {
  219. this.editor.changeViewZones((changeAccessor) => {
  220. if (this._viewZoneId) {
  221. changeAccessor.removeZone(this._viewZoneId);
  222. this._viewZoneId = undefined;
  223. }
  224. });
  225. }
  226. updateLines(lineNumber, additionalLines, minReservedLineCount) {
  227. const textModel = this.editor.getModel();
  228. if (!textModel) {
  229. return;
  230. }
  231. const { tabSize } = textModel.getOptions();
  232. this.editor.changeViewZones((changeAccessor) => {
  233. if (this._viewZoneId) {
  234. changeAccessor.removeZone(this._viewZoneId);
  235. this._viewZoneId = undefined;
  236. }
  237. const heightInLines = Math.max(additionalLines.length, minReservedLineCount);
  238. if (heightInLines > 0) {
  239. const domNode = document.createElement('div');
  240. renderLines(domNode, tabSize, additionalLines, this.editor.getOptions(), this.languageIdCodec);
  241. this._viewZoneId = changeAccessor.addZone({
  242. afterLineNumber: lineNumber,
  243. heightInLines: heightInLines,
  244. domNode,
  245. });
  246. }
  247. });
  248. }
  249. }
  250. function renderLines(domNode, tabSize, lines, opts, languageIdCodec) {
  251. const disableMonospaceOptimizations = opts.get(29 /* disableMonospaceOptimizations */);
  252. const stopRenderingLineAfter = opts.get(104 /* stopRenderingLineAfter */);
  253. // To avoid visual confusion, we don't want to render visible whitespace
  254. const renderWhitespace = 'none';
  255. const renderControlCharacters = opts.get(82 /* renderControlCharacters */);
  256. const fontLigatures = opts.get(44 /* fontLigatures */);
  257. const fontInfo = opts.get(43 /* fontInfo */);
  258. const lineHeight = opts.get(58 /* lineHeight */);
  259. const sb = createStringBuilder(10000);
  260. sb.appendASCIIString('<div class="suggest-preview-text">');
  261. for (let i = 0, len = lines.length; i < len; i++) {
  262. const lineData = lines[i];
  263. const line = lineData.content;
  264. sb.appendASCIIString('<div class="view-line');
  265. sb.appendASCIIString('" style="top:');
  266. sb.appendASCIIString(String(i * lineHeight));
  267. sb.appendASCIIString('px;width:1000000px;">');
  268. const isBasicASCII = strings.isBasicASCII(line);
  269. const containsRTL = strings.containsRTL(line);
  270. const lineTokens = LineTokens.createEmpty(line, languageIdCodec);
  271. renderViewLine(new RenderLineInput((fontInfo.isMonospace && !disableMonospaceOptimizations), fontInfo.canUseHalfwidthRightwardsArrow, line, false, isBasicASCII, containsRTL, 0, lineTokens, lineData.decorations, tabSize, 0, fontInfo.spaceWidth, fontInfo.middotWidth, fontInfo.wsmiddotWidth, stopRenderingLineAfter, renderWhitespace, renderControlCharacters, fontLigatures !== EditorFontLigatures.OFF, null), sb);
  272. sb.appendASCIIString('</div>');
  273. }
  274. sb.appendASCIIString('</div>');
  275. Configuration.applyFontInfoSlow(domNode, fontInfo);
  276. const html = sb.build();
  277. const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html;
  278. domNode.innerHTML = trustedhtml;
  279. }
  280. class ViewMoreLinesContentWidget extends Disposable {
  281. constructor(editor, position, domNode, disposableStore) {
  282. super();
  283. this.editor = editor;
  284. this.position = position;
  285. this.domNode = domNode;
  286. this.allowEditorOverflow = false;
  287. this.suppressMouseDown = false;
  288. this._register(disposableStore);
  289. this._register(toDisposable(() => {
  290. this.editor.removeContentWidget(this);
  291. }));
  292. this.editor.addContentWidget(this);
  293. }
  294. getId() {
  295. return 'editor.widget.viewMoreLinesWidget';
  296. }
  297. getDomNode() {
  298. return this.domNode;
  299. }
  300. getPosition() {
  301. return {
  302. position: this.position,
  303. preference: [0 /* EXACT */]
  304. };
  305. }
  306. }
  307. registerThemingParticipant((theme, collector) => {
  308. const foreground = theme.getColor(ghostTextForeground);
  309. if (foreground) {
  310. // `!important` ensures that other decorations don't cause a style conflict (#132017).
  311. collector.addRule(`.monaco-editor .ghost-text-decoration { color: ${foreground.toString()} !important; }`);
  312. collector.addRule(`.monaco-editor .ghost-text-decoration-preview { color: ${foreground.toString()} !important; }`);
  313. collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { color: ${foreground.toString()} !important; }`);
  314. }
  315. const background = theme.getColor(ghostTextBackground);
  316. if (background) {
  317. collector.addRule(`.monaco-editor .ghost-text-decoration { background-color: ${background.toString()}; }`);
  318. collector.addRule(`.monaco-editor .ghost-text-decoration-preview { background-color: ${background.toString()}; }`);
  319. collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { background-color: ${background.toString()}; }`);
  320. }
  321. const border = theme.getColor(ghostTextBorder);
  322. if (border) {
  323. collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { border: 1px solid ${border}; }`);
  324. collector.addRule(`.monaco-editor .ghost-text-decoration { border: 1px solid ${border}; }`);
  325. collector.addRule(`.monaco-editor .ghost-text-decoration-preview { border: 1px solid ${border}; }`);
  326. }
  327. });