languageFeatures.ts 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267
  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. 'use strict';
  6. import {
  7. Diagnostic,
  8. DiagnosticRelatedInformation,
  9. LanguageServiceDefaults,
  10. typescriptDefaults
  11. } from './monaco.contribution';
  12. import type * as ts from './lib/typescriptServices';
  13. import type { TypeScriptWorker } from './tsWorker';
  14. import { libFileSet } from './lib/lib.index';
  15. import {
  16. editor,
  17. languages,
  18. Uri,
  19. Position,
  20. Range,
  21. CancellationToken,
  22. IDisposable,
  23. IRange,
  24. MarkerTag,
  25. MarkerSeverity
  26. } from './fillers/monaco-editor-core';
  27. //#region utils copied from typescript to prevent loading the entire typescriptServices ---
  28. enum IndentStyle {
  29. None = 0,
  30. Block = 1,
  31. Smart = 2
  32. }
  33. export function flattenDiagnosticMessageText(
  34. diag: string | ts.DiagnosticMessageChain | undefined,
  35. newLine: string,
  36. indent = 0
  37. ): string {
  38. if (typeof diag === 'string') {
  39. return diag;
  40. } else if (diag === undefined) {
  41. return '';
  42. }
  43. let result = '';
  44. if (indent) {
  45. result += newLine;
  46. for (let i = 0; i < indent; i++) {
  47. result += ' ';
  48. }
  49. }
  50. result += diag.messageText;
  51. indent++;
  52. if (diag.next) {
  53. for (const kid of diag.next) {
  54. result += flattenDiagnosticMessageText(kid, newLine, indent);
  55. }
  56. }
  57. return result;
  58. }
  59. function displayPartsToString(displayParts: ts.SymbolDisplayPart[] | undefined): string {
  60. if (displayParts) {
  61. return displayParts.map((displayPart) => displayPart.text).join('');
  62. }
  63. return '';
  64. }
  65. //#endregion
  66. export abstract class Adapter {
  67. constructor(protected _worker: (...uris: Uri[]) => Promise<TypeScriptWorker>) {}
  68. // protected _positionToOffset(model: editor.ITextModel, position: monaco.IPosition): number {
  69. // return model.getOffsetAt(position);
  70. // }
  71. // protected _offsetToPosition(model: editor.ITextModel, offset: number): monaco.IPosition {
  72. // return model.getPositionAt(offset);
  73. // }
  74. protected _textSpanToRange(model: editor.ITextModel, span: ts.TextSpan): IRange {
  75. let p1 = model.getPositionAt(span.start);
  76. let p2 = model.getPositionAt(span.start + span.length);
  77. let { lineNumber: startLineNumber, column: startColumn } = p1;
  78. let { lineNumber: endLineNumber, column: endColumn } = p2;
  79. return { startLineNumber, startColumn, endLineNumber, endColumn };
  80. }
  81. }
  82. // --- lib files
  83. export class LibFiles {
  84. private _libFiles: Record<string, string>;
  85. private _hasFetchedLibFiles: boolean;
  86. private _fetchLibFilesPromise: Promise<void> | null;
  87. constructor(private readonly _worker: (...uris: Uri[]) => Promise<TypeScriptWorker>) {
  88. this._libFiles = {};
  89. this._hasFetchedLibFiles = false;
  90. this._fetchLibFilesPromise = null;
  91. }
  92. public isLibFile(uri: Uri | null): boolean {
  93. if (!uri) {
  94. return false;
  95. }
  96. if (uri.path.indexOf('/lib.') === 0) {
  97. return !!libFileSet[uri.path.slice(1)];
  98. }
  99. return false;
  100. }
  101. public getOrCreateModel(fileName: string): editor.ITextModel | null {
  102. const uri = Uri.parse(fileName);
  103. const model = editor.getModel(uri);
  104. if (model) {
  105. return model;
  106. }
  107. if (this.isLibFile(uri) && this._hasFetchedLibFiles) {
  108. return editor.createModel(this._libFiles[uri.path.slice(1)], 'typescript', uri);
  109. }
  110. const matchedLibFile = typescriptDefaults.getExtraLibs()[fileName];
  111. if (matchedLibFile) {
  112. return editor.createModel(matchedLibFile.content, 'typescript', uri);
  113. }
  114. return null;
  115. }
  116. private _containsLibFile(uris: (Uri | null)[]): boolean {
  117. for (let uri of uris) {
  118. if (this.isLibFile(uri)) {
  119. return true;
  120. }
  121. }
  122. return false;
  123. }
  124. public async fetchLibFilesIfNecessary(uris: (Uri | null)[]): Promise<void> {
  125. if (!this._containsLibFile(uris)) {
  126. // no lib files necessary
  127. return;
  128. }
  129. await this._fetchLibFiles();
  130. }
  131. private _fetchLibFiles(): Promise<void> {
  132. if (!this._fetchLibFilesPromise) {
  133. this._fetchLibFilesPromise = this._worker()
  134. .then((w) => w.getLibFiles())
  135. .then((libFiles) => {
  136. this._hasFetchedLibFiles = true;
  137. this._libFiles = libFiles;
  138. });
  139. }
  140. return this._fetchLibFilesPromise;
  141. }
  142. }
  143. // --- diagnostics --- ---
  144. enum DiagnosticCategory {
  145. Warning = 0,
  146. Error = 1,
  147. Suggestion = 2,
  148. Message = 3
  149. }
  150. /**
  151. * temporary interface until the editor API exposes
  152. * `IModel.isAttachedToEditor` and `IModel.onDidChangeAttached`
  153. */
  154. interface IInternalEditorModel extends editor.IModel {
  155. onDidChangeAttached(listener: () => void): IDisposable;
  156. isAttachedToEditor(): boolean;
  157. }
  158. export class DiagnosticsAdapter extends Adapter {
  159. private _disposables: IDisposable[] = [];
  160. private _listener: { [uri: string]: IDisposable } = Object.create(null);
  161. constructor(
  162. private readonly _libFiles: LibFiles,
  163. private _defaults: LanguageServiceDefaults,
  164. private _selector: string,
  165. worker: (...uris: Uri[]) => Promise<TypeScriptWorker>
  166. ) {
  167. super(worker);
  168. const onModelAdd = (model: IInternalEditorModel): void => {
  169. if (model.getModeId() !== _selector) {
  170. return;
  171. }
  172. const maybeValidate = () => {
  173. const { onlyVisible } = this._defaults.getDiagnosticsOptions();
  174. if (onlyVisible) {
  175. if (model.isAttachedToEditor()) {
  176. this._doValidate(model);
  177. }
  178. } else {
  179. this._doValidate(model);
  180. }
  181. };
  182. let handle: number;
  183. const changeSubscription = model.onDidChangeContent(() => {
  184. clearTimeout(handle);
  185. handle = setTimeout(maybeValidate, 500);
  186. });
  187. const visibleSubscription = model.onDidChangeAttached(() => {
  188. const { onlyVisible } = this._defaults.getDiagnosticsOptions();
  189. if (onlyVisible) {
  190. if (model.isAttachedToEditor()) {
  191. // this model is now attached to an editor
  192. // => compute diagnostics
  193. maybeValidate();
  194. } else {
  195. // this model is no longer attached to an editor
  196. // => clear existing diagnostics
  197. editor.setModelMarkers(model, this._selector, []);
  198. }
  199. }
  200. });
  201. this._listener[model.uri.toString()] = {
  202. dispose() {
  203. changeSubscription.dispose();
  204. visibleSubscription.dispose();
  205. clearTimeout(handle);
  206. }
  207. };
  208. maybeValidate();
  209. };
  210. const onModelRemoved = (model: editor.IModel): void => {
  211. editor.setModelMarkers(model, this._selector, []);
  212. const key = model.uri.toString();
  213. if (this._listener[key]) {
  214. this._listener[key].dispose();
  215. delete this._listener[key];
  216. }
  217. };
  218. this._disposables.push(
  219. editor.onDidCreateModel((model) => onModelAdd(<IInternalEditorModel>model))
  220. );
  221. this._disposables.push(editor.onWillDisposeModel(onModelRemoved));
  222. this._disposables.push(
  223. editor.onDidChangeModelLanguage((event) => {
  224. onModelRemoved(event.model);
  225. onModelAdd(<IInternalEditorModel>event.model);
  226. })
  227. );
  228. this._disposables.push({
  229. dispose() {
  230. for (const model of editor.getModels()) {
  231. onModelRemoved(model);
  232. }
  233. }
  234. });
  235. const recomputeDiagostics = () => {
  236. // redo diagnostics when options change
  237. for (const model of editor.getModels()) {
  238. onModelRemoved(model);
  239. onModelAdd(<IInternalEditorModel>model);
  240. }
  241. };
  242. this._disposables.push(this._defaults.onDidChange(recomputeDiagostics));
  243. this._disposables.push(this._defaults.onDidExtraLibsChange(recomputeDiagostics));
  244. editor.getModels().forEach((model) => onModelAdd(<IInternalEditorModel>model));
  245. }
  246. public dispose(): void {
  247. this._disposables.forEach((d) => d && d.dispose());
  248. this._disposables = [];
  249. }
  250. private async _doValidate(model: editor.ITextModel): Promise<void> {
  251. const worker = await this._worker(model.uri);
  252. if (model.isDisposed()) {
  253. // model was disposed in the meantime
  254. return;
  255. }
  256. const promises: Promise<Diagnostic[]>[] = [];
  257. const {
  258. noSyntaxValidation,
  259. noSemanticValidation,
  260. noSuggestionDiagnostics
  261. } = this._defaults.getDiagnosticsOptions();
  262. if (!noSyntaxValidation) {
  263. promises.push(worker.getSyntacticDiagnostics(model.uri.toString()));
  264. }
  265. if (!noSemanticValidation) {
  266. promises.push(worker.getSemanticDiagnostics(model.uri.toString()));
  267. }
  268. if (!noSuggestionDiagnostics) {
  269. promises.push(worker.getSuggestionDiagnostics(model.uri.toString()));
  270. }
  271. const allDiagnostics = await Promise.all(promises);
  272. if (!allDiagnostics || model.isDisposed()) {
  273. // model was disposed in the meantime
  274. return;
  275. }
  276. const diagnostics = allDiagnostics
  277. .reduce((p, c) => c.concat(p), [])
  278. .filter(
  279. (d) =>
  280. (this._defaults.getDiagnosticsOptions().diagnosticCodesToIgnore || []).indexOf(d.code) ===
  281. -1
  282. );
  283. // Fetch lib files if necessary
  284. const relatedUris = diagnostics
  285. .map((d) => d.relatedInformation || [])
  286. .reduce((p, c) => c.concat(p), [])
  287. .map((relatedInformation) =>
  288. relatedInformation.file ? Uri.parse(relatedInformation.file.fileName) : null
  289. );
  290. await this._libFiles.fetchLibFilesIfNecessary(relatedUris);
  291. if (model.isDisposed()) {
  292. // model was disposed in the meantime
  293. return;
  294. }
  295. editor.setModelMarkers(
  296. model,
  297. this._selector,
  298. diagnostics.map((d) => this._convertDiagnostics(model, d))
  299. );
  300. }
  301. private _convertDiagnostics(model: editor.ITextModel, diag: Diagnostic): editor.IMarkerData {
  302. const diagStart = diag.start || 0;
  303. const diagLength = diag.length || 1;
  304. const { lineNumber: startLineNumber, column: startColumn } = model.getPositionAt(diagStart);
  305. const { lineNumber: endLineNumber, column: endColumn } = model.getPositionAt(
  306. diagStart + diagLength
  307. );
  308. const tags: MarkerTag[] = [];
  309. if (diag.reportsUnnecessary) {
  310. tags.push(MarkerTag.Unnecessary);
  311. }
  312. if (diag.reportsDeprecated) {
  313. tags.push(MarkerTag.Deprecated);
  314. }
  315. return {
  316. severity: this._tsDiagnosticCategoryToMarkerSeverity(diag.category),
  317. startLineNumber,
  318. startColumn,
  319. endLineNumber,
  320. endColumn,
  321. message: flattenDiagnosticMessageText(diag.messageText, '\n'),
  322. code: diag.code.toString(),
  323. tags,
  324. relatedInformation: this._convertRelatedInformation(model, diag.relatedInformation)
  325. };
  326. }
  327. private _convertRelatedInformation(
  328. model: editor.ITextModel,
  329. relatedInformation?: DiagnosticRelatedInformation[]
  330. ): editor.IRelatedInformation[] {
  331. if (!relatedInformation) {
  332. return [];
  333. }
  334. const result: editor.IRelatedInformation[] = [];
  335. relatedInformation.forEach((info) => {
  336. let relatedResource: editor.ITextModel | null = model;
  337. if (info.file) {
  338. relatedResource = this._libFiles.getOrCreateModel(info.file.fileName);
  339. }
  340. if (!relatedResource) {
  341. return;
  342. }
  343. const infoStart = info.start || 0;
  344. const infoLength = info.length || 1;
  345. const { lineNumber: startLineNumber, column: startColumn } = relatedResource.getPositionAt(
  346. infoStart
  347. );
  348. const { lineNumber: endLineNumber, column: endColumn } = relatedResource.getPositionAt(
  349. infoStart + infoLength
  350. );
  351. result.push({
  352. resource: relatedResource.uri,
  353. startLineNumber,
  354. startColumn,
  355. endLineNumber,
  356. endColumn,
  357. message: flattenDiagnosticMessageText(info.messageText, '\n')
  358. });
  359. });
  360. return result;
  361. }
  362. private _tsDiagnosticCategoryToMarkerSeverity(category: ts.DiagnosticCategory): MarkerSeverity {
  363. switch (category) {
  364. case DiagnosticCategory.Error:
  365. return MarkerSeverity.Error;
  366. case DiagnosticCategory.Message:
  367. return MarkerSeverity.Info;
  368. case DiagnosticCategory.Warning:
  369. return MarkerSeverity.Warning;
  370. case DiagnosticCategory.Suggestion:
  371. return MarkerSeverity.Hint;
  372. }
  373. return MarkerSeverity.Info;
  374. }
  375. }
  376. // --- suggest ------
  377. interface MyCompletionItem extends languages.CompletionItem {
  378. label: string;
  379. uri: Uri;
  380. position: Position;
  381. offset: number;
  382. }
  383. export class SuggestAdapter extends Adapter implements languages.CompletionItemProvider {
  384. public get triggerCharacters(): string[] {
  385. return ['.'];
  386. }
  387. public async provideCompletionItems(
  388. model: editor.ITextModel,
  389. position: Position,
  390. _context: languages.CompletionContext,
  391. token: CancellationToken
  392. ): Promise<languages.CompletionList | undefined> {
  393. const wordInfo = model.getWordUntilPosition(position);
  394. const wordRange = new Range(
  395. position.lineNumber,
  396. wordInfo.startColumn,
  397. position.lineNumber,
  398. wordInfo.endColumn
  399. );
  400. const resource = model.uri;
  401. const offset = model.getOffsetAt(position);
  402. const worker = await this._worker(resource);
  403. if (model.isDisposed()) {
  404. return;
  405. }
  406. const info = await worker.getCompletionsAtPosition(resource.toString(), offset);
  407. if (!info || model.isDisposed()) {
  408. return;
  409. }
  410. const suggestions: MyCompletionItem[] = info.entries.map((entry) => {
  411. let range = wordRange;
  412. if (entry.replacementSpan) {
  413. const p1 = model.getPositionAt(entry.replacementSpan.start);
  414. const p2 = model.getPositionAt(entry.replacementSpan.start + entry.replacementSpan.length);
  415. range = new Range(p1.lineNumber, p1.column, p2.lineNumber, p2.column);
  416. }
  417. const tags: languages.CompletionItemTag[] = [];
  418. if (entry.kindModifiers?.indexOf('deprecated') !== -1) {
  419. tags.push(languages.CompletionItemTag.Deprecated);
  420. }
  421. return {
  422. uri: resource,
  423. position: position,
  424. offset: offset,
  425. range: range,
  426. label: entry.name,
  427. insertText: entry.name,
  428. sortText: entry.sortText,
  429. kind: SuggestAdapter.convertKind(entry.kind),
  430. tags
  431. };
  432. });
  433. return {
  434. suggestions
  435. };
  436. }
  437. public async resolveCompletionItem(
  438. item: languages.CompletionItem,
  439. token: CancellationToken
  440. ): Promise<languages.CompletionItem> {
  441. const myItem = <MyCompletionItem>item;
  442. const resource = myItem.uri;
  443. const position = myItem.position;
  444. const offset = myItem.offset;
  445. const worker = await this._worker(resource);
  446. const details = await worker.getCompletionEntryDetails(
  447. resource.toString(),
  448. offset,
  449. myItem.label
  450. );
  451. if (!details) {
  452. return myItem;
  453. }
  454. return <MyCompletionItem>{
  455. uri: resource,
  456. position: position,
  457. label: details.name,
  458. kind: SuggestAdapter.convertKind(details.kind),
  459. detail: displayPartsToString(details.displayParts),
  460. documentation: {
  461. value: SuggestAdapter.createDocumentationString(details)
  462. }
  463. };
  464. }
  465. private static convertKind(kind: string): languages.CompletionItemKind {
  466. switch (kind) {
  467. case Kind.primitiveType:
  468. case Kind.keyword:
  469. return languages.CompletionItemKind.Keyword;
  470. case Kind.variable:
  471. case Kind.localVariable:
  472. return languages.CompletionItemKind.Variable;
  473. case Kind.memberVariable:
  474. case Kind.memberGetAccessor:
  475. case Kind.memberSetAccessor:
  476. return languages.CompletionItemKind.Field;
  477. case Kind.function:
  478. case Kind.memberFunction:
  479. case Kind.constructSignature:
  480. case Kind.callSignature:
  481. case Kind.indexSignature:
  482. return languages.CompletionItemKind.Function;
  483. case Kind.enum:
  484. return languages.CompletionItemKind.Enum;
  485. case Kind.module:
  486. return languages.CompletionItemKind.Module;
  487. case Kind.class:
  488. return languages.CompletionItemKind.Class;
  489. case Kind.interface:
  490. return languages.CompletionItemKind.Interface;
  491. case Kind.warning:
  492. return languages.CompletionItemKind.File;
  493. }
  494. return languages.CompletionItemKind.Property;
  495. }
  496. private static createDocumentationString(details: ts.CompletionEntryDetails): string {
  497. let documentationString = displayPartsToString(details.documentation);
  498. if (details.tags) {
  499. for (const tag of details.tags) {
  500. documentationString += `\n\n${tagToString(tag)}`;
  501. }
  502. }
  503. return documentationString;
  504. }
  505. }
  506. function tagToString(tag: ts.JSDocTagInfo): string {
  507. let tagLabel = `*@${tag.name}*`;
  508. if (tag.name === 'param' && tag.text) {
  509. const [paramName, ...rest] = tag.text;
  510. tagLabel += `\`${paramName.text}\``;
  511. if (rest.length > 0) tagLabel += ` — ${rest.map((r) => r.text).join(' ')}`;
  512. } else if (Array.isArray(tag.text)) {
  513. tagLabel += ` — ${tag.text.map((r) => r.text).join(' ')}`;
  514. } else if (tag.text) {
  515. tagLabel += ` — ${tag.text}`;
  516. }
  517. return tagLabel;
  518. }
  519. export class SignatureHelpAdapter extends Adapter implements languages.SignatureHelpProvider {
  520. public signatureHelpTriggerCharacters = ['(', ','];
  521. private static _toSignatureHelpTriggerReason(
  522. context: languages.SignatureHelpContext
  523. ): ts.SignatureHelpTriggerReason {
  524. switch (context.triggerKind) {
  525. case languages.SignatureHelpTriggerKind.TriggerCharacter:
  526. if (context.triggerCharacter) {
  527. if (context.isRetrigger) {
  528. return { kind: 'retrigger', triggerCharacter: context.triggerCharacter as any };
  529. } else {
  530. return { kind: 'characterTyped', triggerCharacter: context.triggerCharacter as any };
  531. }
  532. } else {
  533. return { kind: 'invoked' };
  534. }
  535. case languages.SignatureHelpTriggerKind.ContentChange:
  536. return context.isRetrigger ? { kind: 'retrigger' } : { kind: 'invoked' };
  537. case languages.SignatureHelpTriggerKind.Invoke:
  538. default:
  539. return { kind: 'invoked' };
  540. }
  541. }
  542. public async provideSignatureHelp(
  543. model: editor.ITextModel,
  544. position: Position,
  545. token: CancellationToken,
  546. context: languages.SignatureHelpContext
  547. ): Promise<languages.SignatureHelpResult | undefined> {
  548. const resource = model.uri;
  549. const offset = model.getOffsetAt(position);
  550. const worker = await this._worker(resource);
  551. if (model.isDisposed()) {
  552. return;
  553. }
  554. const info = await worker.getSignatureHelpItems(resource.toString(), offset, {
  555. triggerReason: SignatureHelpAdapter._toSignatureHelpTriggerReason(context)
  556. });
  557. if (!info || model.isDisposed()) {
  558. return;
  559. }
  560. const ret: languages.SignatureHelp = {
  561. activeSignature: info.selectedItemIndex,
  562. activeParameter: info.argumentIndex,
  563. signatures: []
  564. };
  565. info.items.forEach((item) => {
  566. const signature: languages.SignatureInformation = {
  567. label: '',
  568. parameters: []
  569. };
  570. signature.documentation = {
  571. value: displayPartsToString(item.documentation)
  572. };
  573. signature.label += displayPartsToString(item.prefixDisplayParts);
  574. item.parameters.forEach((p, i, a) => {
  575. const label = displayPartsToString(p.displayParts);
  576. const parameter: languages.ParameterInformation = {
  577. label: label,
  578. documentation: {
  579. value: displayPartsToString(p.documentation)
  580. }
  581. };
  582. signature.label += label;
  583. signature.parameters.push(parameter);
  584. if (i < a.length - 1) {
  585. signature.label += displayPartsToString(item.separatorDisplayParts);
  586. }
  587. });
  588. signature.label += displayPartsToString(item.suffixDisplayParts);
  589. ret.signatures.push(signature);
  590. });
  591. return {
  592. value: ret,
  593. dispose() {}
  594. };
  595. }
  596. }
  597. // --- hover ------
  598. export class QuickInfoAdapter extends Adapter implements languages.HoverProvider {
  599. public async provideHover(
  600. model: editor.ITextModel,
  601. position: Position,
  602. token: CancellationToken
  603. ): Promise<languages.Hover | undefined> {
  604. const resource = model.uri;
  605. const offset = model.getOffsetAt(position);
  606. const worker = await this._worker(resource);
  607. if (model.isDisposed()) {
  608. return;
  609. }
  610. const info = await worker.getQuickInfoAtPosition(resource.toString(), offset);
  611. if (!info || model.isDisposed()) {
  612. return;
  613. }
  614. const documentation = displayPartsToString(info.documentation);
  615. const tags = info.tags ? info.tags.map((tag) => tagToString(tag)).join(' \n\n') : '';
  616. const contents = displayPartsToString(info.displayParts);
  617. return {
  618. range: this._textSpanToRange(model, info.textSpan),
  619. contents: [
  620. {
  621. value: '```typescript\n' + contents + '\n```\n'
  622. },
  623. {
  624. value: documentation + (tags ? '\n\n' + tags : '')
  625. }
  626. ]
  627. };
  628. }
  629. }
  630. // --- occurrences ------
  631. export class OccurrencesAdapter extends Adapter implements languages.DocumentHighlightProvider {
  632. public async provideDocumentHighlights(
  633. model: editor.ITextModel,
  634. position: Position,
  635. token: CancellationToken
  636. ): Promise<languages.DocumentHighlight[] | undefined> {
  637. const resource = model.uri;
  638. const offset = model.getOffsetAt(position);
  639. const worker = await this._worker(resource);
  640. if (model.isDisposed()) {
  641. return;
  642. }
  643. const entries = await worker.getOccurrencesAtPosition(resource.toString(), offset);
  644. if (!entries || model.isDisposed()) {
  645. return;
  646. }
  647. return entries.map((entry) => {
  648. return <languages.DocumentHighlight>{
  649. range: this._textSpanToRange(model, entry.textSpan),
  650. kind: entry.isWriteAccess
  651. ? languages.DocumentHighlightKind.Write
  652. : languages.DocumentHighlightKind.Text
  653. };
  654. });
  655. }
  656. }
  657. // --- definition ------
  658. export class DefinitionAdapter extends Adapter {
  659. constructor(
  660. private readonly _libFiles: LibFiles,
  661. worker: (...uris: Uri[]) => Promise<TypeScriptWorker>
  662. ) {
  663. super(worker);
  664. }
  665. public async provideDefinition(
  666. model: editor.ITextModel,
  667. position: Position,
  668. token: CancellationToken
  669. ): Promise<languages.Definition | undefined> {
  670. const resource = model.uri;
  671. const offset = model.getOffsetAt(position);
  672. const worker = await this._worker(resource);
  673. if (model.isDisposed()) {
  674. return;
  675. }
  676. const entries = await worker.getDefinitionAtPosition(resource.toString(), offset);
  677. if (!entries || model.isDisposed()) {
  678. return;
  679. }
  680. // Fetch lib files if necessary
  681. await this._libFiles.fetchLibFilesIfNecessary(
  682. entries.map((entry) => Uri.parse(entry.fileName))
  683. );
  684. if (model.isDisposed()) {
  685. return;
  686. }
  687. const result: languages.Location[] = [];
  688. for (let entry of entries) {
  689. const refModel = this._libFiles.getOrCreateModel(entry.fileName);
  690. if (refModel) {
  691. result.push({
  692. uri: refModel.uri,
  693. range: this._textSpanToRange(refModel, entry.textSpan)
  694. });
  695. }
  696. }
  697. return result;
  698. }
  699. }
  700. // --- references ------
  701. export class ReferenceAdapter extends Adapter implements languages.ReferenceProvider {
  702. constructor(
  703. private readonly _libFiles: LibFiles,
  704. worker: (...uris: Uri[]) => Promise<TypeScriptWorker>
  705. ) {
  706. super(worker);
  707. }
  708. public async provideReferences(
  709. model: editor.ITextModel,
  710. position: Position,
  711. context: languages.ReferenceContext,
  712. token: CancellationToken
  713. ): Promise<languages.Location[] | undefined> {
  714. const resource = model.uri;
  715. const offset = model.getOffsetAt(position);
  716. const worker = await this._worker(resource);
  717. if (model.isDisposed()) {
  718. return;
  719. }
  720. const entries = await worker.getReferencesAtPosition(resource.toString(), offset);
  721. if (!entries || model.isDisposed()) {
  722. return;
  723. }
  724. // Fetch lib files if necessary
  725. await this._libFiles.fetchLibFilesIfNecessary(
  726. entries.map((entry) => Uri.parse(entry.fileName))
  727. );
  728. if (model.isDisposed()) {
  729. return;
  730. }
  731. const result: languages.Location[] = [];
  732. for (let entry of entries) {
  733. const refModel = this._libFiles.getOrCreateModel(entry.fileName);
  734. if (refModel) {
  735. result.push({
  736. uri: refModel.uri,
  737. range: this._textSpanToRange(refModel, entry.textSpan)
  738. });
  739. }
  740. }
  741. return result;
  742. }
  743. }
  744. // --- outline ------
  745. export class OutlineAdapter extends Adapter implements languages.DocumentSymbolProvider {
  746. public async provideDocumentSymbols(
  747. model: editor.ITextModel,
  748. token: CancellationToken
  749. ): Promise<languages.DocumentSymbol[] | undefined> {
  750. const resource = model.uri;
  751. const worker = await this._worker(resource);
  752. if (model.isDisposed()) {
  753. return;
  754. }
  755. const items = await worker.getNavigationBarItems(resource.toString());
  756. if (!items || model.isDisposed()) {
  757. return;
  758. }
  759. const convert = (
  760. bucket: languages.DocumentSymbol[],
  761. item: ts.NavigationBarItem,
  762. containerLabel?: string
  763. ): void => {
  764. let result: languages.DocumentSymbol = {
  765. name: item.text,
  766. detail: '',
  767. kind: <languages.SymbolKind>(outlineTypeTable[item.kind] || languages.SymbolKind.Variable),
  768. range: this._textSpanToRange(model, item.spans[0]),
  769. selectionRange: this._textSpanToRange(model, item.spans[0]),
  770. tags: []
  771. };
  772. if (containerLabel) result.containerName = containerLabel;
  773. if (item.childItems && item.childItems.length > 0) {
  774. for (let child of item.childItems) {
  775. convert(bucket, child, result.name);
  776. }
  777. }
  778. bucket.push(result);
  779. };
  780. let result: languages.DocumentSymbol[] = [];
  781. items.forEach((item) => convert(result, item));
  782. return result;
  783. }
  784. }
  785. export class Kind {
  786. public static unknown: string = '';
  787. public static keyword: string = 'keyword';
  788. public static script: string = 'script';
  789. public static module: string = 'module';
  790. public static class: string = 'class';
  791. public static interface: string = 'interface';
  792. public static type: string = 'type';
  793. public static enum: string = 'enum';
  794. public static variable: string = 'var';
  795. public static localVariable: string = 'local var';
  796. public static function: string = 'function';
  797. public static localFunction: string = 'local function';
  798. public static memberFunction: string = 'method';
  799. public static memberGetAccessor: string = 'getter';
  800. public static memberSetAccessor: string = 'setter';
  801. public static memberVariable: string = 'property';
  802. public static constructorImplementation: string = 'constructor';
  803. public static callSignature: string = 'call';
  804. public static indexSignature: string = 'index';
  805. public static constructSignature: string = 'construct';
  806. public static parameter: string = 'parameter';
  807. public static typeParameter: string = 'type parameter';
  808. public static primitiveType: string = 'primitive type';
  809. public static label: string = 'label';
  810. public static alias: string = 'alias';
  811. public static const: string = 'const';
  812. public static let: string = 'let';
  813. public static warning: string = 'warning';
  814. }
  815. let outlineTypeTable: {
  816. [kind: string]: languages.SymbolKind;
  817. } = Object.create(null);
  818. outlineTypeTable[Kind.module] = languages.SymbolKind.Module;
  819. outlineTypeTable[Kind.class] = languages.SymbolKind.Class;
  820. outlineTypeTable[Kind.enum] = languages.SymbolKind.Enum;
  821. outlineTypeTable[Kind.interface] = languages.SymbolKind.Interface;
  822. outlineTypeTable[Kind.memberFunction] = languages.SymbolKind.Method;
  823. outlineTypeTable[Kind.memberVariable] = languages.SymbolKind.Property;
  824. outlineTypeTable[Kind.memberGetAccessor] = languages.SymbolKind.Property;
  825. outlineTypeTable[Kind.memberSetAccessor] = languages.SymbolKind.Property;
  826. outlineTypeTable[Kind.variable] = languages.SymbolKind.Variable;
  827. outlineTypeTable[Kind.const] = languages.SymbolKind.Variable;
  828. outlineTypeTable[Kind.localVariable] = languages.SymbolKind.Variable;
  829. outlineTypeTable[Kind.variable] = languages.SymbolKind.Variable;
  830. outlineTypeTable[Kind.function] = languages.SymbolKind.Function;
  831. outlineTypeTable[Kind.localFunction] = languages.SymbolKind.Function;
  832. // --- formatting ----
  833. export abstract class FormatHelper extends Adapter {
  834. protected static _convertOptions(options: languages.FormattingOptions): ts.FormatCodeOptions {
  835. return {
  836. ConvertTabsToSpaces: options.insertSpaces,
  837. TabSize: options.tabSize,
  838. IndentSize: options.tabSize,
  839. IndentStyle: IndentStyle.Smart,
  840. NewLineCharacter: '\n',
  841. InsertSpaceAfterCommaDelimiter: true,
  842. InsertSpaceAfterSemicolonInForStatements: true,
  843. InsertSpaceBeforeAndAfterBinaryOperators: true,
  844. InsertSpaceAfterKeywordsInControlFlowStatements: true,
  845. InsertSpaceAfterFunctionKeywordForAnonymousFunctions: true,
  846. InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,
  847. InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false,
  848. InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false,
  849. PlaceOpenBraceOnNewLineForControlBlocks: false,
  850. PlaceOpenBraceOnNewLineForFunctions: false
  851. };
  852. }
  853. protected _convertTextChanges(
  854. model: editor.ITextModel,
  855. change: ts.TextChange
  856. ): languages.TextEdit {
  857. return {
  858. text: change.newText,
  859. range: this._textSpanToRange(model, change.span)
  860. };
  861. }
  862. }
  863. export class FormatAdapter
  864. extends FormatHelper
  865. implements languages.DocumentRangeFormattingEditProvider {
  866. public async provideDocumentRangeFormattingEdits(
  867. model: editor.ITextModel,
  868. range: Range,
  869. options: languages.FormattingOptions,
  870. token: CancellationToken
  871. ): Promise<languages.TextEdit[] | undefined> {
  872. const resource = model.uri;
  873. const startOffset = model.getOffsetAt({
  874. lineNumber: range.startLineNumber,
  875. column: range.startColumn
  876. });
  877. const endOffset = model.getOffsetAt({
  878. lineNumber: range.endLineNumber,
  879. column: range.endColumn
  880. });
  881. const worker = await this._worker(resource);
  882. if (model.isDisposed()) {
  883. return;
  884. }
  885. const edits = await worker.getFormattingEditsForRange(
  886. resource.toString(),
  887. startOffset,
  888. endOffset,
  889. FormatHelper._convertOptions(options)
  890. );
  891. if (!edits || model.isDisposed()) {
  892. return;
  893. }
  894. return edits.map((edit) => this._convertTextChanges(model, edit));
  895. }
  896. }
  897. export class FormatOnTypeAdapter
  898. extends FormatHelper
  899. implements languages.OnTypeFormattingEditProvider {
  900. get autoFormatTriggerCharacters() {
  901. return [';', '}', '\n'];
  902. }
  903. public async provideOnTypeFormattingEdits(
  904. model: editor.ITextModel,
  905. position: Position,
  906. ch: string,
  907. options: languages.FormattingOptions,
  908. token: CancellationToken
  909. ): Promise<languages.TextEdit[] | undefined> {
  910. const resource = model.uri;
  911. const offset = model.getOffsetAt(position);
  912. const worker = await this._worker(resource);
  913. if (model.isDisposed()) {
  914. return;
  915. }
  916. const edits = await worker.getFormattingEditsAfterKeystroke(
  917. resource.toString(),
  918. offset,
  919. ch,
  920. FormatHelper._convertOptions(options)
  921. );
  922. if (!edits || model.isDisposed()) {
  923. return;
  924. }
  925. return edits.map((edit) => this._convertTextChanges(model, edit));
  926. }
  927. }
  928. // --- code actions ------
  929. export class CodeActionAdaptor extends FormatHelper implements languages.CodeActionProvider {
  930. public async provideCodeActions(
  931. model: editor.ITextModel,
  932. range: Range,
  933. context: languages.CodeActionContext,
  934. token: CancellationToken
  935. ): Promise<languages.CodeActionList | undefined> {
  936. const resource = model.uri;
  937. const start = model.getOffsetAt({
  938. lineNumber: range.startLineNumber,
  939. column: range.startColumn
  940. });
  941. const end = model.getOffsetAt({
  942. lineNumber: range.endLineNumber,
  943. column: range.endColumn
  944. });
  945. const formatOptions = FormatHelper._convertOptions(model.getOptions());
  946. const errorCodes = context.markers
  947. .filter((m) => m.code)
  948. .map((m) => m.code)
  949. .map(Number);
  950. const worker = await this._worker(resource);
  951. if (model.isDisposed()) {
  952. return;
  953. }
  954. const codeFixes = await worker.getCodeFixesAtPosition(
  955. resource.toString(),
  956. start,
  957. end,
  958. errorCodes,
  959. formatOptions
  960. );
  961. if (!codeFixes || model.isDisposed()) {
  962. return { actions: [], dispose: () => {} };
  963. }
  964. const actions = codeFixes
  965. .filter((fix) => {
  966. // Removes any 'make a new file'-type code fix
  967. return fix.changes.filter((change) => change.isNewFile).length === 0;
  968. })
  969. .map((fix) => {
  970. return this._tsCodeFixActionToMonacoCodeAction(model, context, fix);
  971. });
  972. return {
  973. actions: actions,
  974. dispose: () => {}
  975. };
  976. }
  977. private _tsCodeFixActionToMonacoCodeAction(
  978. model: editor.ITextModel,
  979. context: languages.CodeActionContext,
  980. codeFix: ts.CodeFixAction
  981. ): languages.CodeAction {
  982. const edits: languages.WorkspaceTextEdit[] = [];
  983. for (const change of codeFix.changes) {
  984. for (const textChange of change.textChanges) {
  985. edits.push({
  986. resource: model.uri,
  987. edit: {
  988. range: this._textSpanToRange(model, textChange.span),
  989. text: textChange.newText
  990. }
  991. });
  992. }
  993. }
  994. const action: languages.CodeAction = {
  995. title: codeFix.description,
  996. edit: { edits: edits },
  997. diagnostics: context.markers,
  998. kind: 'quickfix'
  999. };
  1000. return action;
  1001. }
  1002. }
  1003. // --- rename ----
  1004. export class RenameAdapter extends Adapter implements languages.RenameProvider {
  1005. constructor(
  1006. private readonly _libFiles: LibFiles,
  1007. worker: (...uris: Uri[]) => Promise<TypeScriptWorker>
  1008. ) {
  1009. super(worker);
  1010. }
  1011. public async provideRenameEdits(
  1012. model: editor.ITextModel,
  1013. position: Position,
  1014. newName: string,
  1015. token: CancellationToken
  1016. ): Promise<(languages.WorkspaceEdit & languages.Rejection) | undefined> {
  1017. const resource = model.uri;
  1018. const fileName = resource.toString();
  1019. const offset = model.getOffsetAt(position);
  1020. const worker = await this._worker(resource);
  1021. if (model.isDisposed()) {
  1022. return;
  1023. }
  1024. const renameInfo = await worker.getRenameInfo(fileName, offset, {
  1025. allowRenameOfImportPath: false
  1026. });
  1027. if (renameInfo.canRename === false) {
  1028. // use explicit comparison so that the discriminated union gets resolved properly
  1029. return {
  1030. edits: [],
  1031. rejectReason: renameInfo.localizedErrorMessage
  1032. };
  1033. }
  1034. if (renameInfo.fileToRename !== undefined) {
  1035. throw new Error('Renaming files is not supported.');
  1036. }
  1037. const renameLocations = await worker.findRenameLocations(
  1038. fileName,
  1039. offset,
  1040. /*strings*/ false,
  1041. /*comments*/ false,
  1042. /*prefixAndSuffix*/ false
  1043. );
  1044. if (!renameLocations || model.isDisposed()) {
  1045. return;
  1046. }
  1047. const edits: languages.WorkspaceTextEdit[] = [];
  1048. for (const renameLocation of renameLocations) {
  1049. const model = this._libFiles.getOrCreateModel(renameLocation.fileName);
  1050. if (model) {
  1051. edits.push({
  1052. resource: model.uri,
  1053. edit: {
  1054. range: this._textSpanToRange(model, renameLocation.textSpan),
  1055. text: newName
  1056. }
  1057. });
  1058. } else {
  1059. throw new Error(`Unknown file ${renameLocation.fileName}.`);
  1060. }
  1061. }
  1062. return { edits };
  1063. }
  1064. }
  1065. // --- inlay hints ----
  1066. export class InlayHintsAdapter extends Adapter implements languages.InlayHintsProvider {
  1067. public async provideInlayHints(
  1068. model: editor.ITextModel,
  1069. range: Range,
  1070. token: CancellationToken
  1071. ): Promise<languages.InlayHint[]> {
  1072. const resource = model.uri;
  1073. const fileName = resource.toString();
  1074. const start = model.getOffsetAt({
  1075. lineNumber: range.startLineNumber,
  1076. column: range.startColumn
  1077. });
  1078. const end = model.getOffsetAt({
  1079. lineNumber: range.endLineNumber,
  1080. column: range.endColumn
  1081. });
  1082. const worker = await this._worker(resource);
  1083. if (model.isDisposed()) {
  1084. return [];
  1085. }
  1086. const hints = await worker.provideInlayHints(fileName, start, end);
  1087. return hints.map((hint) => {
  1088. return {
  1089. ...hint,
  1090. position: model.getPositionAt(hint.position),
  1091. kind: this._convertHintKind(hint.kind)
  1092. };
  1093. });
  1094. }
  1095. private _convertHintKind(kind?: ts.InlayHintKind) {
  1096. switch (kind) {
  1097. case 'Parameter':
  1098. return languages.InlayHintKind.Parameter;
  1099. case 'Type':
  1100. return languages.InlayHintKind.Type;
  1101. default:
  1102. return languages.InlayHintKind.Other;
  1103. }
  1104. }
  1105. }