links.js 17 KB

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