languageFeatures.ts 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150
  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) tags.push(MarkerTag.Unnecessary);
  290. if (diag.reportsDeprecated) tags.push(MarkerTag.Deprecated);
  291. return {
  292. severity: this._tsDiagnosticCategoryToMarkerSeverity(diag.category),
  293. startLineNumber,
  294. startColumn,
  295. endLineNumber,
  296. endColumn,
  297. message: flattenDiagnosticMessageText(diag.messageText, '\n'),
  298. code: diag.code.toString(),
  299. tags,
  300. relatedInformation: this._convertRelatedInformation(
  301. model,
  302. diag.relatedInformation
  303. )
  304. };
  305. }
  306. private _convertRelatedInformation(
  307. model: editor.ITextModel,
  308. relatedInformation?: ts.DiagnosticRelatedInformation[]
  309. ): editor.IRelatedInformation[] | undefined {
  310. if (!relatedInformation) {
  311. return;
  312. }
  313. const result: editor.IRelatedInformation[] = [];
  314. relatedInformation.forEach((info) => {
  315. let relatedResource: editor.ITextModel | null = model;
  316. if (info.file) {
  317. const relatedResourceUri = Uri.parse(info.file.fileName);
  318. relatedResource = this._libFiles.getOrCreateModel(relatedResourceUri);
  319. }
  320. if (!relatedResource) {
  321. return;
  322. }
  323. const infoStart = info.start || 0;
  324. const infoLength = info.length || 1;
  325. const {
  326. lineNumber: startLineNumber,
  327. column: startColumn
  328. } = relatedResource.getPositionAt(infoStart);
  329. const {
  330. lineNumber: endLineNumber,
  331. column: endColumn
  332. } = relatedResource.getPositionAt(infoStart + infoLength);
  333. result.push({
  334. resource: relatedResource.uri,
  335. startLineNumber,
  336. startColumn,
  337. endLineNumber,
  338. endColumn,
  339. message: flattenDiagnosticMessageText(info.messageText, '\n')
  340. });
  341. });
  342. return result;
  343. }
  344. private _tsDiagnosticCategoryToMarkerSeverity(
  345. category: ts.DiagnosticCategory
  346. ): MarkerSeverity {
  347. switch (category) {
  348. case DiagnosticCategory.Error:
  349. return MarkerSeverity.Error;
  350. case DiagnosticCategory.Message:
  351. return MarkerSeverity.Info;
  352. case DiagnosticCategory.Warning:
  353. return MarkerSeverity.Warning;
  354. case DiagnosticCategory.Suggestion:
  355. return MarkerSeverity.Hint;
  356. }
  357. return MarkerSeverity.Info;
  358. }
  359. }
  360. // --- suggest ------
  361. interface MyCompletionItem extends languages.CompletionItem {
  362. label: string;
  363. uri: Uri;
  364. position: Position;
  365. }
  366. export class SuggestAdapter
  367. extends Adapter
  368. implements languages.CompletionItemProvider {
  369. public get triggerCharacters(): string[] {
  370. return ['.'];
  371. }
  372. public async provideCompletionItems(
  373. model: editor.ITextModel,
  374. position: Position,
  375. _context: languages.CompletionContext,
  376. token: CancellationToken
  377. ): Promise<languages.CompletionList | undefined> {
  378. const wordInfo = model.getWordUntilPosition(position);
  379. const wordRange = new Range(
  380. position.lineNumber,
  381. wordInfo.startColumn,
  382. position.lineNumber,
  383. wordInfo.endColumn
  384. );
  385. const resource = model.uri;
  386. const offset = model.getOffsetAt(position);
  387. const worker = await this._worker(resource);
  388. const info = await worker.getCompletionsAtPosition(
  389. resource.toString(),
  390. offset
  391. );
  392. if (!info || model.isDisposed()) {
  393. return;
  394. }
  395. const suggestions: MyCompletionItem[] = info.entries.map((entry) => {
  396. let range = wordRange;
  397. if (entry.replacementSpan) {
  398. const p1 = model.getPositionAt(entry.replacementSpan.start);
  399. const p2 = model.getPositionAt(
  400. entry.replacementSpan.start + entry.replacementSpan.length
  401. );
  402. range = new Range(p1.lineNumber, p1.column, p2.lineNumber, p2.column);
  403. }
  404. const tags: languages.CompletionItemTag[] = [];
  405. if (entry.kindModifiers?.indexOf('deprecated') !== -1)
  406. tags.push(languages.CompletionItemTag.Deprecated);
  407. return {
  408. uri: resource,
  409. position: position,
  410. range: range,
  411. label: entry.name,
  412. insertText: entry.name,
  413. sortText: entry.sortText,
  414. kind: SuggestAdapter.convertKind(entry.kind),
  415. tags
  416. };
  417. });
  418. return {
  419. suggestions
  420. };
  421. }
  422. public async resolveCompletionItem(
  423. model: editor.ITextModel,
  424. _position: Position,
  425. item: languages.CompletionItem,
  426. token: CancellationToken
  427. ): Promise<languages.CompletionItem> {
  428. const myItem = <MyCompletionItem>item;
  429. const resource = myItem.uri;
  430. const position = myItem.position;
  431. const offset = model.getOffsetAt(position);
  432. const worker = await this._worker(resource);
  433. const details = await worker.getCompletionEntryDetails(
  434. resource.toString(),
  435. offset,
  436. myItem.label
  437. );
  438. if (!details || model.isDisposed()) {
  439. return myItem;
  440. }
  441. return <MyCompletionItem>{
  442. uri: resource,
  443. position: position,
  444. label: details.name,
  445. kind: SuggestAdapter.convertKind(details.kind),
  446. detail: displayPartsToString(details.displayParts),
  447. documentation: {
  448. value: SuggestAdapter.createDocumentationString(details)
  449. }
  450. };
  451. }
  452. private static convertKind(kind: string): languages.CompletionItemKind {
  453. switch (kind) {
  454. case Kind.primitiveType:
  455. case Kind.keyword:
  456. return languages.CompletionItemKind.Keyword;
  457. case Kind.variable:
  458. case Kind.localVariable:
  459. return languages.CompletionItemKind.Variable;
  460. case Kind.memberVariable:
  461. case Kind.memberGetAccessor:
  462. case Kind.memberSetAccessor:
  463. return languages.CompletionItemKind.Field;
  464. case Kind.function:
  465. case Kind.memberFunction:
  466. case Kind.constructSignature:
  467. case Kind.callSignature:
  468. case Kind.indexSignature:
  469. return languages.CompletionItemKind.Function;
  470. case Kind.enum:
  471. return languages.CompletionItemKind.Enum;
  472. case Kind.module:
  473. return languages.CompletionItemKind.Module;
  474. case Kind.class:
  475. return languages.CompletionItemKind.Class;
  476. case Kind.interface:
  477. return languages.CompletionItemKind.Interface;
  478. case Kind.warning:
  479. return languages.CompletionItemKind.File;
  480. }
  481. return languages.CompletionItemKind.Property;
  482. }
  483. private static createDocumentationString(
  484. details: ts.CompletionEntryDetails
  485. ): string {
  486. let documentationString = displayPartsToString(details.documentation);
  487. if (details.tags) {
  488. for (const tag of details.tags) {
  489. documentationString += `\n\n${tagToString(tag)}`;
  490. }
  491. }
  492. return documentationString;
  493. }
  494. }
  495. function tagToString(tag: ts.JSDocTagInfo): string {
  496. let tagLabel = `*@${tag.name}*`;
  497. if (tag.name === 'param' && tag.text) {
  498. const [paramName, ...rest] = tag.text.split(' ');
  499. tagLabel += `\`${paramName}\``;
  500. if (rest.length > 0) tagLabel += ` — ${rest.join(' ')}`;
  501. } else if (tag.text) {
  502. tagLabel += ` — ${tag.text}`;
  503. }
  504. return tagLabel;
  505. }
  506. export class SignatureHelpAdapter
  507. extends Adapter
  508. implements languages.SignatureHelpProvider {
  509. public signatureHelpTriggerCharacters = ['(', ','];
  510. public async provideSignatureHelp(
  511. model: editor.ITextModel,
  512. position: Position,
  513. token: CancellationToken
  514. ): Promise<languages.SignatureHelpResult | undefined> {
  515. const resource = model.uri;
  516. const offset = model.getOffsetAt(position);
  517. const worker = await this._worker(resource);
  518. const info = await worker.getSignatureHelpItems(
  519. resource.toString(),
  520. offset
  521. );
  522. if (!info || model.isDisposed()) {
  523. return;
  524. }
  525. const ret: languages.SignatureHelp = {
  526. activeSignature: info.selectedItemIndex,
  527. activeParameter: info.argumentIndex,
  528. signatures: []
  529. };
  530. info.items.forEach((item) => {
  531. const signature: languages.SignatureInformation = {
  532. label: '',
  533. parameters: []
  534. };
  535. signature.documentation = {
  536. value: displayPartsToString(item.documentation)
  537. };
  538. signature.label += displayPartsToString(item.prefixDisplayParts);
  539. item.parameters.forEach((p, i, a) => {
  540. const label = displayPartsToString(p.displayParts);
  541. const parameter: languages.ParameterInformation = {
  542. label: label,
  543. documentation: {
  544. value: displayPartsToString(p.documentation)
  545. }
  546. };
  547. signature.label += label;
  548. signature.parameters.push(parameter);
  549. if (i < a.length - 1) {
  550. signature.label += displayPartsToString(item.separatorDisplayParts);
  551. }
  552. });
  553. signature.label += displayPartsToString(item.suffixDisplayParts);
  554. ret.signatures.push(signature);
  555. });
  556. return {
  557. value: ret,
  558. dispose() {}
  559. };
  560. }
  561. }
  562. // --- hover ------
  563. export class QuickInfoAdapter
  564. extends Adapter
  565. implements languages.HoverProvider {
  566. public async provideHover(
  567. model: editor.ITextModel,
  568. position: Position,
  569. token: CancellationToken
  570. ): Promise<languages.Hover | undefined> {
  571. const resource = model.uri;
  572. const offset = model.getOffsetAt(position);
  573. const worker = await this._worker(resource);
  574. const info = await worker.getQuickInfoAtPosition(
  575. resource.toString(),
  576. offset
  577. );
  578. if (!info || model.isDisposed()) {
  579. return;
  580. }
  581. const documentation = displayPartsToString(info.documentation);
  582. const tags = info.tags
  583. ? info.tags.map((tag) => tagToString(tag)).join(' \n\n')
  584. : '';
  585. const contents = displayPartsToString(info.displayParts);
  586. return {
  587. range: this._textSpanToRange(model, info.textSpan),
  588. contents: [
  589. {
  590. value: '```js\n' + contents + '\n```\n'
  591. },
  592. {
  593. value: documentation + (tags ? '\n\n' + tags : '')
  594. }
  595. ]
  596. };
  597. }
  598. }
  599. // --- occurrences ------
  600. export class OccurrencesAdapter
  601. extends Adapter
  602. implements languages.DocumentHighlightProvider {
  603. public async provideDocumentHighlights(
  604. model: editor.ITextModel,
  605. position: Position,
  606. token: CancellationToken
  607. ): Promise<languages.DocumentHighlight[] | undefined> {
  608. const resource = model.uri;
  609. const offset = model.getOffsetAt(position);
  610. const worker = await this._worker(resource);
  611. const entries = await worker.getOccurrencesAtPosition(
  612. resource.toString(),
  613. offset
  614. );
  615. if (!entries || model.isDisposed()) {
  616. return;
  617. }
  618. return entries.map((entry) => {
  619. return <languages.DocumentHighlight>{
  620. range: this._textSpanToRange(model, entry.textSpan),
  621. kind: entry.isWriteAccess
  622. ? languages.DocumentHighlightKind.Write
  623. : languages.DocumentHighlightKind.Text
  624. };
  625. });
  626. }
  627. }
  628. // --- definition ------
  629. export class DefinitionAdapter extends Adapter {
  630. constructor(
  631. private readonly _libFiles: LibFiles,
  632. worker: (...uris: Uri[]) => Promise<TypeScriptWorker>
  633. ) {
  634. super(worker);
  635. }
  636. public async provideDefinition(
  637. model: editor.ITextModel,
  638. position: Position,
  639. token: CancellationToken
  640. ): Promise<languages.Definition | undefined> {
  641. const resource = model.uri;
  642. const offset = model.getOffsetAt(position);
  643. const worker = await this._worker(resource);
  644. const entries = await worker.getDefinitionAtPosition(
  645. resource.toString(),
  646. offset
  647. );
  648. if (!entries || model.isDisposed()) {
  649. return;
  650. }
  651. // Fetch lib files if necessary
  652. await this._libFiles.fetchLibFilesIfNecessary(
  653. entries.map((entry) => Uri.parse(entry.fileName))
  654. );
  655. if (model.isDisposed()) {
  656. return;
  657. }
  658. const result: languages.Location[] = [];
  659. for (let entry of entries) {
  660. const uri = Uri.parse(entry.fileName);
  661. const refModel = this._libFiles.getOrCreateModel(uri);
  662. if (refModel) {
  663. result.push({
  664. uri: uri,
  665. range: this._textSpanToRange(refModel, entry.textSpan)
  666. });
  667. }
  668. }
  669. return result;
  670. }
  671. }
  672. // --- references ------
  673. export class ReferenceAdapter
  674. extends Adapter
  675. implements languages.ReferenceProvider {
  676. constructor(
  677. private readonly _libFiles: LibFiles,
  678. worker: (...uris: Uri[]) => Promise<TypeScriptWorker>
  679. ) {
  680. super(worker);
  681. }
  682. public async provideReferences(
  683. model: editor.ITextModel,
  684. position: Position,
  685. context: languages.ReferenceContext,
  686. token: CancellationToken
  687. ): Promise<languages.Location[] | undefined> {
  688. const resource = model.uri;
  689. const offset = model.getOffsetAt(position);
  690. const worker = await this._worker(resource);
  691. const entries = await worker.getReferencesAtPosition(
  692. resource.toString(),
  693. offset
  694. );
  695. if (!entries || model.isDisposed()) {
  696. return;
  697. }
  698. // Fetch lib files if necessary
  699. await this._libFiles.fetchLibFilesIfNecessary(
  700. entries.map((entry) => Uri.parse(entry.fileName))
  701. );
  702. if (model.isDisposed()) {
  703. return;
  704. }
  705. const result: languages.Location[] = [];
  706. for (let entry of entries) {
  707. const uri = Uri.parse(entry.fileName);
  708. const refModel = this._libFiles.getOrCreateModel(uri);
  709. if (refModel) {
  710. result.push({
  711. uri: uri,
  712. range: this._textSpanToRange(refModel, entry.textSpan)
  713. });
  714. }
  715. }
  716. return result;
  717. }
  718. }
  719. // --- outline ------
  720. export class OutlineAdapter
  721. extends Adapter
  722. implements languages.DocumentSymbolProvider {
  723. public async provideDocumentSymbols(
  724. model: editor.ITextModel,
  725. token: CancellationToken
  726. ): Promise<languages.DocumentSymbol[] | undefined> {
  727. const resource = model.uri;
  728. const worker = await this._worker(resource);
  729. const items = await worker.getNavigationBarItems(resource.toString());
  730. if (!items || model.isDisposed()) {
  731. return;
  732. }
  733. const convert = (
  734. bucket: languages.DocumentSymbol[],
  735. item: ts.NavigationBarItem,
  736. containerLabel?: string
  737. ): void => {
  738. let result: languages.DocumentSymbol = {
  739. name: item.text,
  740. detail: '',
  741. kind: <languages.SymbolKind>(
  742. (outlineTypeTable[item.kind] || languages.SymbolKind.Variable)
  743. ),
  744. range: this._textSpanToRange(model, item.spans[0]),
  745. selectionRange: this._textSpanToRange(model, item.spans[0]),
  746. tags: [],
  747. containerName: containerLabel
  748. };
  749. if (item.childItems && item.childItems.length > 0) {
  750. for (let child of item.childItems) {
  751. convert(bucket, child, result.name);
  752. }
  753. }
  754. bucket.push(result);
  755. };
  756. let result: languages.DocumentSymbol[] = [];
  757. items.forEach((item) => convert(result, item));
  758. return result;
  759. }
  760. }
  761. export class Kind {
  762. public static unknown: string = '';
  763. public static keyword: string = 'keyword';
  764. public static script: string = 'script';
  765. public static module: string = 'module';
  766. public static class: string = 'class';
  767. public static interface: string = 'interface';
  768. public static type: string = 'type';
  769. public static enum: string = 'enum';
  770. public static variable: string = 'var';
  771. public static localVariable: string = 'local var';
  772. public static function: string = 'function';
  773. public static localFunction: string = 'local function';
  774. public static memberFunction: string = 'method';
  775. public static memberGetAccessor: string = 'getter';
  776. public static memberSetAccessor: string = 'setter';
  777. public static memberVariable: string = 'property';
  778. public static constructorImplementation: string = 'constructor';
  779. public static callSignature: string = 'call';
  780. public static indexSignature: string = 'index';
  781. public static constructSignature: string = 'construct';
  782. public static parameter: string = 'parameter';
  783. public static typeParameter: string = 'type parameter';
  784. public static primitiveType: string = 'primitive type';
  785. public static label: string = 'label';
  786. public static alias: string = 'alias';
  787. public static const: string = 'const';
  788. public static let: string = 'let';
  789. public static warning: string = 'warning';
  790. }
  791. let outlineTypeTable: {
  792. [kind: string]: languages.SymbolKind;
  793. } = Object.create(null);
  794. outlineTypeTable[Kind.module] = languages.SymbolKind.Module;
  795. outlineTypeTable[Kind.class] = languages.SymbolKind.Class;
  796. outlineTypeTable[Kind.enum] = languages.SymbolKind.Enum;
  797. outlineTypeTable[Kind.interface] = languages.SymbolKind.Interface;
  798. outlineTypeTable[Kind.memberFunction] = languages.SymbolKind.Method;
  799. outlineTypeTable[Kind.memberVariable] = languages.SymbolKind.Property;
  800. outlineTypeTable[Kind.memberGetAccessor] = languages.SymbolKind.Property;
  801. outlineTypeTable[Kind.memberSetAccessor] = languages.SymbolKind.Property;
  802. outlineTypeTable[Kind.variable] = languages.SymbolKind.Variable;
  803. outlineTypeTable[Kind.const] = languages.SymbolKind.Variable;
  804. outlineTypeTable[Kind.localVariable] = languages.SymbolKind.Variable;
  805. outlineTypeTable[Kind.variable] = languages.SymbolKind.Variable;
  806. outlineTypeTable[Kind.function] = languages.SymbolKind.Function;
  807. outlineTypeTable[Kind.localFunction] = languages.SymbolKind.Function;
  808. // --- formatting ----
  809. export abstract class FormatHelper extends Adapter {
  810. protected static _convertOptions(
  811. options: languages.FormattingOptions
  812. ): ts.FormatCodeOptions {
  813. return {
  814. ConvertTabsToSpaces: options.insertSpaces,
  815. TabSize: options.tabSize,
  816. IndentSize: options.tabSize,
  817. IndentStyle: IndentStyle.Smart,
  818. NewLineCharacter: '\n',
  819. InsertSpaceAfterCommaDelimiter: true,
  820. InsertSpaceAfterSemicolonInForStatements: true,
  821. InsertSpaceBeforeAndAfterBinaryOperators: true,
  822. InsertSpaceAfterKeywordsInControlFlowStatements: true,
  823. InsertSpaceAfterFunctionKeywordForAnonymousFunctions: true,
  824. InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,
  825. InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false,
  826. InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false,
  827. PlaceOpenBraceOnNewLineForControlBlocks: false,
  828. PlaceOpenBraceOnNewLineForFunctions: false
  829. };
  830. }
  831. protected _convertTextChanges(
  832. model: editor.ITextModel,
  833. change: ts.TextChange
  834. ): languages.TextEdit {
  835. return {
  836. text: change.newText,
  837. range: this._textSpanToRange(model, change.span)
  838. };
  839. }
  840. }
  841. export class FormatAdapter
  842. extends FormatHelper
  843. implements languages.DocumentRangeFormattingEditProvider {
  844. public async provideDocumentRangeFormattingEdits(
  845. model: editor.ITextModel,
  846. range: Range,
  847. options: languages.FormattingOptions,
  848. token: CancellationToken
  849. ): Promise<languages.TextEdit[] | undefined> {
  850. const resource = model.uri;
  851. const startOffset = model.getOffsetAt({
  852. lineNumber: range.startLineNumber,
  853. column: range.startColumn
  854. });
  855. const endOffset = model.getOffsetAt({
  856. lineNumber: range.endLineNumber,
  857. column: range.endColumn
  858. });
  859. const worker = await this._worker(resource);
  860. const edits = await worker.getFormattingEditsForRange(
  861. resource.toString(),
  862. startOffset,
  863. endOffset,
  864. FormatHelper._convertOptions(options)
  865. );
  866. if (!edits || model.isDisposed()) {
  867. return;
  868. }
  869. return edits.map((edit) => this._convertTextChanges(model, edit));
  870. }
  871. }
  872. export class FormatOnTypeAdapter
  873. extends FormatHelper
  874. implements languages.OnTypeFormattingEditProvider {
  875. get autoFormatTriggerCharacters() {
  876. return [';', '}', '\n'];
  877. }
  878. public async provideOnTypeFormattingEdits(
  879. model: editor.ITextModel,
  880. position: Position,
  881. ch: string,
  882. options: languages.FormattingOptions,
  883. token: CancellationToken
  884. ): Promise<languages.TextEdit[] | undefined> {
  885. const resource = model.uri;
  886. const offset = model.getOffsetAt(position);
  887. const worker = await this._worker(resource);
  888. const edits = await worker.getFormattingEditsAfterKeystroke(
  889. resource.toString(),
  890. offset,
  891. ch,
  892. FormatHelper._convertOptions(options)
  893. );
  894. if (!edits || model.isDisposed()) {
  895. return;
  896. }
  897. return edits.map((edit) => this._convertTextChanges(model, edit));
  898. }
  899. }
  900. // --- code actions ------
  901. export class CodeActionAdaptor
  902. extends FormatHelper
  903. implements languages.CodeActionProvider {
  904. public async provideCodeActions(
  905. model: editor.ITextModel,
  906. range: Range,
  907. context: languages.CodeActionContext,
  908. token: CancellationToken
  909. ): Promise<languages.CodeActionList> {
  910. const resource = model.uri;
  911. const start = model.getOffsetAt({
  912. lineNumber: range.startLineNumber,
  913. column: range.startColumn
  914. });
  915. const end = model.getOffsetAt({
  916. lineNumber: range.endLineNumber,
  917. column: range.endColumn
  918. });
  919. const formatOptions = FormatHelper._convertOptions(model.getOptions());
  920. const errorCodes = context.markers
  921. .filter((m) => m.code)
  922. .map((m) => m.code)
  923. .map(Number);
  924. const worker = await this._worker(resource);
  925. const codeFixes = await worker.getCodeFixesAtPosition(
  926. resource.toString(),
  927. start,
  928. end,
  929. errorCodes,
  930. formatOptions
  931. );
  932. if (!codeFixes || model.isDisposed()) {
  933. return { actions: [], dispose: () => {} };
  934. }
  935. const actions = codeFixes
  936. .filter((fix) => {
  937. // Removes any 'make a new file'-type code fix
  938. return fix.changes.filter((change) => change.isNewFile).length === 0;
  939. })
  940. .map((fix) => {
  941. return this._tsCodeFixActionToMonacoCodeAction(model, context, fix);
  942. });
  943. return {
  944. actions: actions,
  945. dispose: () => {}
  946. };
  947. }
  948. private _tsCodeFixActionToMonacoCodeAction(
  949. model: editor.ITextModel,
  950. context: languages.CodeActionContext,
  951. codeFix: ts.CodeFixAction
  952. ): languages.CodeAction {
  953. const edits: languages.WorkspaceTextEdit[] = [];
  954. for (const change of codeFix.changes) {
  955. for (const textChange of change.textChanges) {
  956. edits.push({
  957. resource: model.uri,
  958. edit: {
  959. range: this._textSpanToRange(model, textChange.span),
  960. text: textChange.newText
  961. }
  962. });
  963. }
  964. }
  965. const action: languages.CodeAction = {
  966. title: codeFix.description,
  967. edit: { edits: edits },
  968. diagnostics: context.markers,
  969. kind: 'quickfix'
  970. };
  971. return action;
  972. }
  973. }
  974. // --- rename ----
  975. export class RenameAdapter extends Adapter implements languages.RenameProvider {
  976. public async provideRenameEdits(
  977. model: editor.ITextModel,
  978. position: Position,
  979. newName: string,
  980. token: CancellationToken
  981. ): Promise<(languages.WorkspaceEdit & languages.Rejection) | undefined> {
  982. const resource = model.uri;
  983. const fileName = resource.toString();
  984. const offset = model.getOffsetAt(position);
  985. const worker = await this._worker(resource);
  986. const renameInfo = await worker.getRenameInfo(fileName, offset, {
  987. allowRenameOfImportPath: false
  988. });
  989. if (renameInfo.canRename === false) {
  990. // use explicit comparison so that the discriminated union gets resolved properly
  991. return {
  992. edits: [],
  993. rejectReason: renameInfo.localizedErrorMessage
  994. };
  995. }
  996. if (renameInfo.fileToRename !== undefined) {
  997. throw new Error('Renaming files is not supported.');
  998. }
  999. const renameLocations = await worker.findRenameLocations(
  1000. fileName,
  1001. offset,
  1002. /*strings*/ false,
  1003. /*comments*/ false,
  1004. /*prefixAndSuffix*/ false
  1005. );
  1006. if (!renameLocations || model.isDisposed()) {
  1007. return;
  1008. }
  1009. const edits: languages.WorkspaceTextEdit[] = [];
  1010. for (const renameLocation of renameLocations) {
  1011. edits.push({
  1012. resource: Uri.parse(renameLocation.fileName),
  1013. edit: {
  1014. range: this._textSpanToRange(model, renameLocation.textSpan),
  1015. text: newName
  1016. }
  1017. });
  1018. }
  1019. return { edits };
  1020. }
  1021. }