e4e5b8aabb5e4ac99c11ce697b06d9791c9b74441b6e6f9ff0e94090f814c89ae8957613e0c201a849126d0aef0521886aa6da261413a4698b4ef74098c5ab 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  15. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  16. return new (P || (P = Promise))(function (resolve, reject) {
  17. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  18. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  19. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  20. step((generator = generator.apply(thisArg, _arguments || [])).next());
  21. });
  22. };
  23. import { createCancelablePromise, RunOnceScheduler } from '../../../../base/common/async.js';
  24. import { CancellationToken } from '../../../../base/common/cancellation.js';
  25. import { onUnexpectedError } from '../../../../base/common/errors.js';
  26. import { MarkdownString } from '../../../../base/common/htmlContent.js';
  27. import { Disposable } from '../../../../base/common/lifecycle.js';
  28. import { Schemas } from '../../../../base/common/network.js';
  29. import * as platform from '../../../../base/common/platform.js';
  30. import * as resources from '../../../../base/common/resources.js';
  31. import { StopWatch } from '../../../../base/common/stopwatch.js';
  32. import { URI } from '../../../../base/common/uri.js';
  33. import './links.css';
  34. import { EditorAction, registerEditorAction, registerEditorContribution } from '../../../browser/editorExtensions.js';
  35. import { ModelDecorationOptions } from '../../../common/model/textModel.js';
  36. import { ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';
  37. import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
  38. import { ClickLinkGesture } from '../../gotoSymbol/browser/link/clickLinkGesture.js';
  39. import { getLinks } from './getLinks.js';
  40. import * as nls from '../../../../nls.js';
  41. import { INotificationService } from '../../../../platform/notification/common/notification.js';
  42. import { IOpenerService } from '../../../../platform/opener/common/opener.js';
  43. import { editorActiveLinkForeground } from '../../../../platform/theme/common/colorRegistry.js';
  44. import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
  45. let LinkDetector = class LinkDetector extends Disposable {
  46. constructor(editor, openerService, notificationService, languageFeaturesService, languageFeatureDebounceService) {
  47. super();
  48. this.editor = editor;
  49. this.openerService = openerService;
  50. this.notificationService = notificationService;
  51. this.languageFeaturesService = languageFeaturesService;
  52. this.providers = this.languageFeaturesService.linkProvider;
  53. this.debounceInformation = languageFeatureDebounceService.for(this.providers, 'Links', { min: 1000, max: 4000 });
  54. this.computeLinks = this._register(new RunOnceScheduler(() => this.computeLinksNow(), 1000));
  55. this.computePromise = null;
  56. this.activeLinksList = null;
  57. this.currentOccurrences = {};
  58. this.activeLinkDecorationId = null;
  59. const clickLinkGesture = this._register(new ClickLinkGesture(editor));
  60. this._register(clickLinkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {
  61. this._onEditorMouseMove(mouseEvent, keyboardEvent);
  62. }));
  63. this._register(clickLinkGesture.onExecute((e) => {
  64. this.onEditorMouseUp(e);
  65. }));
  66. this._register(clickLinkGesture.onCancel((e) => {
  67. this.cleanUpActiveLinkDecoration();
  68. }));
  69. this._register(editor.onDidChangeConfiguration((e) => {
  70. if (!e.hasChanged(65 /* EditorOption.links */)) {
  71. return;
  72. }
  73. // Remove any links (for the getting disabled case)
  74. this.updateDecorations([]);
  75. // Stop any computation (for the getting disabled case)
  76. this.stop();
  77. // Start computing (for the getting enabled case)
  78. this.computeLinks.schedule(0);
  79. }));
  80. this._register(editor.onDidChangeModelContent((e) => {
  81. if (!this.editor.hasModel()) {
  82. return;
  83. }
  84. this.computeLinks.schedule(this.debounceInformation.get(this.editor.getModel()));
  85. }));
  86. this._register(editor.onDidChangeModel((e) => {
  87. this.currentOccurrences = {};
  88. this.activeLinkDecorationId = null;
  89. this.stop();
  90. this.computeLinks.schedule(0);
  91. }));
  92. this._register(editor.onDidChangeModelLanguage((e) => {
  93. this.stop();
  94. this.computeLinks.schedule(0);
  95. }));
  96. this._register(this.providers.onDidChange((e) => {
  97. this.stop();
  98. this.computeLinks.schedule(0);
  99. }));
  100. this.computeLinks.schedule(0);
  101. }
  102. static get(editor) {
  103. return editor.getContribution(LinkDetector.ID);
  104. }
  105. computeLinksNow() {
  106. return __awaiter(this, void 0, void 0, function* () {
  107. if (!this.editor.hasModel() || !this.editor.getOption(65 /* EditorOption.links */)) {
  108. return;
  109. }
  110. const model = this.editor.getModel();
  111. if (!this.providers.has(model)) {
  112. return;
  113. }
  114. if (this.activeLinksList) {
  115. this.activeLinksList.dispose();
  116. this.activeLinksList = null;
  117. }
  118. this.computePromise = createCancelablePromise(token => getLinks(this.providers, model, token));
  119. try {
  120. const sw = new StopWatch(false);
  121. this.activeLinksList = yield this.computePromise;
  122. this.debounceInformation.update(model, sw.elapsed());
  123. if (model.isDisposed()) {
  124. return;
  125. }
  126. this.updateDecorations(this.activeLinksList.links);
  127. }
  128. catch (err) {
  129. onUnexpectedError(err);
  130. }
  131. finally {
  132. this.computePromise = null;
  133. }
  134. });
  135. }
  136. updateDecorations(links) {
  137. const useMetaKey = (this.editor.getOption(72 /* EditorOption.multiCursorModifier */) === 'altKey');
  138. const oldDecorations = [];
  139. const keys = Object.keys(this.currentOccurrences);
  140. for (const decorationId of keys) {
  141. const occurence = this.currentOccurrences[decorationId];
  142. oldDecorations.push(occurence.decorationId);
  143. }
  144. const newDecorations = [];
  145. if (links) {
  146. // Not sure why this is sometimes null
  147. for (const link of links) {
  148. newDecorations.push(LinkOccurrence.decoration(link, useMetaKey));
  149. }
  150. }
  151. this.editor.changeDecorations((changeAccessor) => {
  152. const decorations = changeAccessor.deltaDecorations(oldDecorations, newDecorations);
  153. this.currentOccurrences = {};
  154. this.activeLinkDecorationId = null;
  155. for (let i = 0, len = decorations.length; i < len; i++) {
  156. const occurence = new LinkOccurrence(links[i], decorations[i]);
  157. this.currentOccurrences[occurence.decorationId] = occurence;
  158. }
  159. });
  160. }
  161. _onEditorMouseMove(mouseEvent, withKey) {
  162. const useMetaKey = (this.editor.getOption(72 /* EditorOption.multiCursorModifier */) === 'altKey');
  163. if (this.isEnabled(mouseEvent, withKey)) {
  164. this.cleanUpActiveLinkDecoration(); // always remove previous link decoration as their can only be one
  165. const occurrence = this.getLinkOccurrence(mouseEvent.target.position);
  166. if (occurrence) {
  167. this.editor.changeDecorations((changeAccessor) => {
  168. occurrence.activate(changeAccessor, useMetaKey);
  169. this.activeLinkDecorationId = occurrence.decorationId;
  170. });
  171. }
  172. }
  173. else {
  174. this.cleanUpActiveLinkDecoration();
  175. }
  176. }
  177. cleanUpActiveLinkDecoration() {
  178. const useMetaKey = (this.editor.getOption(72 /* EditorOption.multiCursorModifier */) === 'altKey');
  179. if (this.activeLinkDecorationId) {
  180. const occurrence = this.currentOccurrences[this.activeLinkDecorationId];
  181. if (occurrence) {
  182. this.editor.changeDecorations((changeAccessor) => {
  183. occurrence.deactivate(changeAccessor, useMetaKey);
  184. });
  185. }
  186. this.activeLinkDecorationId = null;
  187. }
  188. }
  189. onEditorMouseUp(mouseEvent) {
  190. if (!this.isEnabled(mouseEvent)) {
  191. return;
  192. }
  193. const occurrence = this.getLinkOccurrence(mouseEvent.target.position);
  194. if (!occurrence) {
  195. return;
  196. }
  197. this.openLinkOccurrence(occurrence, mouseEvent.hasSideBySideModifier, true /* from user gesture */);
  198. }
  199. openLinkOccurrence(occurrence, openToSide, fromUserGesture = false) {
  200. if (!this.openerService) {
  201. return;
  202. }
  203. const { link } = occurrence;
  204. link.resolve(CancellationToken.None).then(uri => {
  205. // Support for relative file URIs of the shape file://./relativeFile.txt or file:///./relativeFile.txt
  206. if (typeof uri === 'string' && this.editor.hasModel()) {
  207. const modelUri = this.editor.getModel().uri;
  208. if (modelUri.scheme === Schemas.file && uri.startsWith(`${Schemas.file}:`)) {
  209. const parsedUri = URI.parse(uri);
  210. if (parsedUri.scheme === Schemas.file) {
  211. const fsPath = resources.originalFSPath(parsedUri);
  212. let relativePath = null;
  213. if (fsPath.startsWith('/./')) {
  214. relativePath = `.${fsPath.substr(1)}`;
  215. }
  216. else if (fsPath.startsWith('//./')) {
  217. relativePath = `.${fsPath.substr(2)}`;
  218. }
  219. if (relativePath) {
  220. uri = resources.joinPath(modelUri, relativePath);
  221. }
  222. }
  223. }
  224. }
  225. return this.openerService.open(uri, { openToSide, fromUserGesture, allowContributedOpeners: true, allowCommands: true, fromWorkspace: true });
  226. }, err => {
  227. const messageOrError = err instanceof Error ? err.message : err;
  228. // different error cases
  229. if (messageOrError === 'invalid') {
  230. this.notificationService.warn(nls.localize('invalid.url', 'Failed to open this link because it is not well-formed: {0}', link.url.toString()));
  231. }
  232. else if (messageOrError === 'missing') {
  233. this.notificationService.warn(nls.localize('missing.url', 'Failed to open this link because its target is missing.'));
  234. }
  235. else {
  236. onUnexpectedError(err);
  237. }
  238. });
  239. }
  240. getLinkOccurrence(position) {
  241. if (!this.editor.hasModel() || !position) {
  242. return null;
  243. }
  244. const decorations = this.editor.getModel().getDecorationsInRange({
  245. startLineNumber: position.lineNumber,
  246. startColumn: position.column,
  247. endLineNumber: position.lineNumber,
  248. endColumn: position.column
  249. }, 0, true);
  250. for (const decoration of decorations) {
  251. const currentOccurrence = this.currentOccurrences[decoration.id];
  252. if (currentOccurrence) {
  253. return currentOccurrence;
  254. }
  255. }
  256. return null;
  257. }
  258. isEnabled(mouseEvent, withKey) {
  259. return Boolean((mouseEvent.target.type === 6 /* MouseTargetType.CONTENT_TEXT */)
  260. && (mouseEvent.hasTriggerModifier || (withKey && withKey.keyCodeIsTriggerKey)));
  261. }
  262. stop() {
  263. var _a;
  264. this.computeLinks.cancel();
  265. if (this.activeLinksList) {
  266. (_a = this.activeLinksList) === null || _a === void 0 ? void 0 : _a.dispose();
  267. this.activeLinksList = null;
  268. }
  269. if (this.computePromise) {
  270. this.computePromise.cancel();
  271. this.computePromise = null;
  272. }
  273. }
  274. dispose() {
  275. super.dispose();
  276. this.stop();
  277. }
  278. };
  279. LinkDetector.ID = 'editor.linkDetector';
  280. LinkDetector = __decorate([
  281. __param(1, IOpenerService),
  282. __param(2, INotificationService),
  283. __param(3, ILanguageFeaturesService),
  284. __param(4, ILanguageFeatureDebounceService)
  285. ], LinkDetector);
  286. export { LinkDetector };
  287. const decoration = {
  288. general: ModelDecorationOptions.register({
  289. description: 'detected-link',
  290. stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */,
  291. collapseOnReplaceEdit: true,
  292. inlineClassName: 'detected-link'
  293. }),
  294. active: ModelDecorationOptions.register({
  295. description: 'detected-link-active',
  296. stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */,
  297. collapseOnReplaceEdit: true,
  298. inlineClassName: 'detected-link-active'
  299. })
  300. };
  301. class LinkOccurrence {
  302. constructor(link, decorationId) {
  303. this.link = link;
  304. this.decorationId = decorationId;
  305. }
  306. static decoration(link, useMetaKey) {
  307. return {
  308. range: link.range,
  309. options: LinkOccurrence._getOptions(link, useMetaKey, false)
  310. };
  311. }
  312. static _getOptions(link, useMetaKey, isActive) {
  313. const options = Object.assign({}, (isActive ? decoration.active : decoration.general));
  314. options.hoverMessage = getHoverMessage(link, useMetaKey);
  315. return options;
  316. }
  317. activate(changeAccessor, useMetaKey) {
  318. changeAccessor.changeDecorationOptions(this.decorationId, LinkOccurrence._getOptions(this.link, useMetaKey, true));
  319. }
  320. deactivate(changeAccessor, useMetaKey) {
  321. changeAccessor.changeDecorationOptions(this.decorationId, LinkOccurrence._getOptions(this.link, useMetaKey, false));
  322. }
  323. }
  324. function getHoverMessage(link, useMetaKey) {
  325. const executeCmd = link.url && /^command:/i.test(link.url.toString());
  326. const label = link.tooltip
  327. ? link.tooltip
  328. : executeCmd
  329. ? nls.localize('links.navigate.executeCmd', 'Execute command')
  330. : nls.localize('links.navigate.follow', 'Follow link');
  331. const kb = useMetaKey
  332. ? platform.isMacintosh
  333. ? nls.localize('links.navigate.kb.meta.mac', "cmd + click")
  334. : nls.localize('links.navigate.kb.meta', "ctrl + click")
  335. : platform.isMacintosh
  336. ? nls.localize('links.navigate.kb.alt.mac', "option + click")
  337. : nls.localize('links.navigate.kb.alt', "alt + click");
  338. if (link.url) {
  339. let nativeLabel = '';
  340. if (/^command:/i.test(link.url.toString())) {
  341. // Don't show complete command arguments in the native tooltip
  342. const match = link.url.toString().match(/^command:([^?#]+)/);
  343. if (match) {
  344. const commandId = match[1];
  345. nativeLabel = nls.localize('tooltip.explanation', "Execute command {0}", commandId);
  346. }
  347. }
  348. const hoverMessage = new MarkdownString('', true)
  349. .appendLink(link.url.toString(true).replace(/ /g, '%20'), label, nativeLabel)
  350. .appendMarkdown(` (${kb})`);
  351. return hoverMessage;
  352. }
  353. else {
  354. return new MarkdownString().appendText(`${label} (${kb})`);
  355. }
  356. }
  357. class OpenLinkAction extends EditorAction {
  358. constructor() {
  359. super({
  360. id: 'editor.action.openLink',
  361. label: nls.localize('label', "Open Link"),
  362. alias: 'Open Link',
  363. precondition: undefined
  364. });
  365. }
  366. run(accessor, editor) {
  367. const linkDetector = LinkDetector.get(editor);
  368. if (!linkDetector) {
  369. return;
  370. }
  371. if (!editor.hasModel()) {
  372. return;
  373. }
  374. const selections = editor.getSelections();
  375. for (const sel of selections) {
  376. const link = linkDetector.getLinkOccurrence(sel.getEndPosition());
  377. if (link) {
  378. linkDetector.openLinkOccurrence(link, false);
  379. }
  380. }
  381. }
  382. }
  383. registerEditorContribution(LinkDetector.ID, LinkDetector);
  384. registerEditorAction(OpenLinkAction);
  385. registerThemingParticipant((theme, collector) => {
  386. const activeLinkForeground = theme.getColor(editorActiveLinkForeground);
  387. if (activeLinkForeground) {
  388. collector.addRule(`.monaco-editor .detected-link-active { color: ${activeLinkForeground} !important; }`);
  389. }
  390. });