Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(language-server): simplify startServer abstraction #113

Merged
merged 3 commits into from
Dec 19, 2023

Conversation

johnsoncodehk
Copy link
Member

@johnsoncodehk johnsoncodehk commented Dec 19, 2023

  • Removed ServerPlugin abstraction: The ServerPlugin abstraction, which was previously used for implementing Monoserver. Monoserver is now replaced by Hybrid Mode (TS Plugin + LSP Server), making the Plugin API unnecessary.

  • Removed volar.config.js support: The convention of using a config file (volar.config.js) is no longer needed for the framework. If a server config file is required, it should be implemented by the tool itself.

  • API refactoring: The naming convention for server creation functions has been changed from start*Server to createServer.

    For example, consider the server plugin in [v2] request handlers and context access in ServerPlugin #110. The previous implementation:

    import { createConnection, startTypeScriptServer } from '@volar/language-server/node';
    
    const connection = createConnection();
    const serverPlugin = ({ modules }) => {
      return {
        typescript: {
          extraFileExtensions: [],
        },
    
        watchFileExtensions: [
          'cjs',
          'cts',
          'js',
          'jsx',
          'json',
          'mjs',
          'mts',
          'ts',
          'tsx',
        ],
    
        async resolveConfig(config, env, projectContext) {
          if (!modules.typescript) {
            throw new Error('TypeScript module is missing');
          }
    
          (config.services ||= {}).typescript = createTypeScriptService(
            modules.typescript,
          );
    
          return config;
        },
        onInitialized(projects) {
          connection.onRequest(helloRequest, async ({ textDocument, name }) => {
            if (!textDocument) {
              connection.console.log(`Hello, ${name}!`);
              return true;
            }
            const languageService = await getTypeScriptLanguageService(
              textDocument.uri,
            );
    
            connection.console.log(
              `Hello in "${languageService
                .getProgram()
                ?.getCurrentDirectory()}", ${name}!`,
            );
            return true;
          });
    
          async function getTypeScriptLanguageService(uri: string) {
            return (await projects.getProject(uri))
              .getLanguageService()
              .context.inject<Provide, 'typescript/languageService'>(
                'typescript/languageService',
              );
          }
        },
      };
    };
    startTypeScriptServer(connection, serverPlugin);

    It should now be refactored to the following implementation:

    import { createConnection, createNodeServer, createTypeScriptProjectProvider } from '@volar/language-server/node';
    
    const connection = createConnection();
    const server = createNodeServer(connection);
    
    connection.listen();
    
    connection.onInitialize(params => {
      return server.initialize(params, createTypeScriptProjectProvider, {
        typescript: {
          extraFileExtensions: [],
        },
        watchFileExtensions: [
          'cjs',
          'cts',
          'js',
          'jsx',
          'json',
          'mjs',
          'mts',
          'ts',
          'tsx',
        ],
        getServerCapabilitiesSetup: getProjectSetup,
        getProjectSetup: getProjectSetup,
      });
    });
    
    connection.onInitialized(() => {
      server.initialized();
    });
    
    connection.onRequest(helloRequest, async ({ textDocument, name }) => {
      if (!textDocument) {
        connection.console.log(`Hello, ${name}!`);
        return true;
      }
      const languageService = await getTypeScriptLanguageService(
        textDocument.uri,
      );
    
      connection.console.log(
        `Hello in "${languageService
          .getProgram()
          ?.getCurrentDirectory()}", ${name}!`,
      );
      return true;
    });
    
    connection.onShutdown(() => {
      server.shutdown();
    });
    
    function getProjectSetup() {
      return {
        languagePlugins: [],
        servicePlugins: [createTypeScriptService(getTsLib())],
      }
    }
    
    function getTsLib() {
      const ts = server.modules.typescript;
      if (!ts) {
        throw new Error('TypeScript module is missing');
      }
      return ts;
    }
    
    async function getTypeScriptLanguageService(uri: string) {
      return (await server.projects.getProject(uri))
        .getLanguageService()
        .context.inject<Provide, 'typescript/languageService'>(
          'typescript/languageService',
        );
    }

@johnsoncodehk johnsoncodehk linked an issue Dec 19, 2023 that may be closed by this pull request
@johnsoncodehk
Copy link
Member Author

Please note that this is still not a stable API... 😅 we may randomly discover API improvements in alpha releases.

@remcohaszing
Copy link
Member

I understand the reasons for wanting to use a TypeScript plugin, but when trying mdx-js/mdx-analyzer#371, I found it to not be very stable yet. Can we keep using the TypeScript service with this new abstraction?

Also what would the registration of other language plugins / service plugins look like? I.e. https://github.com/mdx-js/mdx-analyzer/blob/31f1912328a688d2e5c943f3f8923c7fee75cc98/packages/language-server/lib/language-server-plugin.js#L46-L52

Also I suppose these changes make #109 obsolete?

@johnsoncodehk
Copy link
Member Author

johnsoncodehk commented Dec 19, 2023

I understand the reasons for wanting to use a TypeScript plugin, but when trying mdx-js/mdx-analyzer#371, I found it to not be very stable yet. Can we keep using the TypeScript service with this new abstraction?

Sure you can! That PR is just a reference for implementation but not a request. Please feel free to decide whether to migrate it.

On the other hand, I can also change that PR to support both, just expose an IDE option to let users choose to use LSP or TS Plugin. The implementation is very simple, just decide whether to pass in createTypeScriptProjectProvider or createSimpeProjectProvider according to the client init options.

connection.onInitialize(params => {
-  return server.initialize(params, createTypeScriptProjectProvider, {
+  return server.initialize(params, params.initializationOptions.yourOption ? createTypeScriptProjectProvider : createSimpleProjectProvider, {

Also what would the registration of other language plugins / service plugins look like? I.e. mdx-js/mdx-analyzer@31f1912/packages/language-server/lib/language-server-plugin.js#L46-L52

It's looks like this:

async getServerCapabilitiesSetup() {
	return {
		servicePlugins: [
			createMarkdownService(),
			createMdxServicePlugin(),
			createTypeScriptService(server.modules.typescript),
		],
	};
},
async getProjectSetup(serviceEnv, projectContext) {

	const plugins = await loadPlugins(
		projectContext.typescript?.configFileName,
		server.modules.typescript
	)

	return {
		languagePlugins: [
			createMdxLanguagePlugin(plugins),
		],
		servicePlugins: [
			createMarkdownService(),
			createMdxServicePlugin(),
			createTypeScriptService(server.modules.typescript),
		],
	};
}

Also I suppose these changes make #109 obsolete?

Yes, as volar.config.js is no longer supported by framework.

@remcohaszing
Copy link
Member

Does this mean it’s not possible to resolve plugins based on tsconfig if TypeScript isn’t used?

@johnsoncodehk
Copy link
Member Author

Does this mean it’s not possible to resolve plugins based on tsconfig if TypeScript isn’t used?

Yes, actually Vue has the same problem (vueCompilerOptions.plugin in tsconfig), I will research the solution.

@johnsoncodehk johnsoncodehk merged commit 7b9ef8e into master Dec 19, 2023
6 checks passed
@johnsoncodehk johnsoncodehk deleted the simplify-server branch December 19, 2023 15:29
@Andarist
Copy link

In the proposed refactor of my snippet I can see:

    getServerCapabilitiesSetup: getProjectSetup,
    getProjectSetup: getProjectSetup,

but getProjectSetup has not been provided in the example. Would you mind including it?

@johnsoncodehk
Copy link
Member Author

@Andarist my mistake, it's updated now. 👌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[v2] request handlers and context access in ServerPlugin
3 participants