languageFeatures.ts 30 KB

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