volarjs/volar.js

Cannot extend `info.languageService` asynchronously with `createAsyncLanguageServicePlugin`

Opened this issue · 0 comments

mizdra commented

Summary

I am developing TypeScript Language Service Plugin with createAsyncLanguageServicePlugin. I want to extend info.languageService to add language features. For example, I want to always return the keyword "Hello" from getCompletionsAtPosition.

However, when I do that, tsserver doesn't seem to load the extended functionality.

Reproduction

repository: https://github.com/mizdra/repro-volar-setup-with-async-language-service

  1. Execute npm i in the terminal.
  2. Open this repository in VS Code.
  3. Open example/index.ts in the editor.
  4. When "Would you like to use the workspace TypeScript version for TypeScript and JavaScript language features?" prompt appears, select "Allow".
  5. Press Ctrl+Space on example/index.ts and check the completion items

src/index.cjs:

const { proxyLanguageService } = require('./language-service.cjs');
const { createAsyncLanguageServicePlugin } = require('@volar/typescript/lib/quickstart/createAsyncLanguageServicePlugin.js');
const ts = require('typescript');

module.exports = createAsyncLanguageServicePlugin([], ts.ScriptKind.TS, async (ts, info) => {
  if (info.project.projectKind !== ts.server.ProjectKind.Configured) {
    return { languagePlugins: [] };
  }

  return {
    languagePlugins: [],
    setup: (language) => {
      info.languageService = proxyLanguageService(info.languageService);
    },
  };
});

src/language-service.cjs:

const ts = require('typescript');

/**
 * @param {import('typescript/lib/tsserverlibrary').LanguageService} languageService 
 * @returns {import('typescript/lib/tsserverlibrary').LanguageService}
 */
exports.proxyLanguageService = function proxyLanguageService(
  languageService,
) {
  /** @type {import('typescript/lib/tsserverlibrary').LanguageService} */
  const proxy = Object.create(null);
  for (const k of Object.keys(languageService)) {
    // @ts-ignore
    const x = (languageService)[k];
    // @ts-ignore
    proxy[k] = (...args) => x.apply(languageService, args);
  }

  proxy.getCompletionsAtPosition = (fileName, position, options) => {
    const prior = languageService.getCompletionsAtPosition(fileName, position, options) ?? {
      isGlobalCompletion: false,
      isMemberCompletion: false,
      isNewIdentifierLocation: false,
      entries: [],
    };
    prior.entries.push({
      name: 'Hello',
      kind: ts.ScriptElementKind.keyword,
      sortText: '0',
    });
    return prior;
  };
  return proxy;
}

Expected behavior

The completion items contain "Hello".

Actual behavior

The completion items do not contain "Hello".

image

Additional context

Apparently, extending info.languageService asynchronously reproduces the problem. If I extend it synchronously as follows, the problem does not reproduce.

--- a/src/index.cjs
+++ b/src/index.cjs
@@ -6,11 +6,11 @@ module.exports = createAsyncLanguageServicePlugin([], ts.ScriptKind.TS, async (t
   if (info.project.projectKind !== ts.server.ProjectKind.Configured) {
     return { languagePlugins: [] };
   }
+  info.languageService = proxyLanguageService(info.languageService);
 
   return {
     languagePlugins: [],
     setup: (language) => {
-      info.languageService = proxyLanguageService(info.languageService);
     },
   };
 });
image

However, if I insert asynchronous operations as follows, the problem reproduces.

--- a/src/index.cjs
+++ b/src/index.cjs
@@ -1,3 +1,4 @@
+const { setTimeout } = require('node:timers/promises');
 const { proxyLanguageService } = require('./language-service.cjs');
 const { createAsyncLanguageServicePlugin } = require('@volar/typescript/lib/quickstart/createAsyncLanguageServicePlugin.js');
 const ts = require('typescript');
@@ -6,11 +7,12 @@ module.exports = createAsyncLanguageServicePlugin([], ts.ScriptKind.TS, async (t
   if (info.project.projectKind !== ts.server.ProjectKind.Configured) {
     return { languagePlugins: [] };
   }
+  await setTimeout(0);
+  info.languageService = proxyLanguageService(info.languageService);
 
   return {
     languagePlugins: [],
     setup: (language) => {
-      info.languageService = proxyLanguageService(info.languageService);
     },
image

Environment

  • VSCode: 1.96.2
  • OS: Darwin arm64 24.1.0
  • @volar/typescript: 2.4.11
  • @volar/language-core: 2.4.11