languageFeatures.ts 32 KB

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