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

[APPS-41865] Add LSP plugin interface with pygls language server to formalize communication with vs code extension #1225

Closed
wants to merge 6 commits into from

Conversation

sfc-gh-klin
Copy link

@sfc-gh-klin sfc-gh-klin commented Jun 18, 2024

Pre-review checklist

  • I've confirmed that instructions included in README.md are still correct after my changes in the codebase.
  • I've added or updated automated unit tests to verify correctness of my new code.
  • I've added or updated integration tests to verify correctness of my new code.
  • I've confirmed that my changes are working by executing CLI's commands manually.
  • I've confirmed that my changes are up-to-date with the target branch.
  • I've described my changes in the release notes.
  • I've described my changes in the section below.

Changes description

...

We propose adding code into SnowCLI that allows the VS Code extension to start a JSON-RPC Pygls language server and provides a new interface for “LSP plugins” so that the VS Code extension for Snowflake can make requests to this long-lived language server to invoke commands such as Native Apps commands instead of spawning one-off processes through the command line tool.

This lets us communicate between the two without parsing command line output. We can also formalize version compatibility rather than have the potential of SnowCLI changes breaking the VS Code extension for Snowflake.

Doc here
VS Code extension for Snowflake PR here: https://github.com/snowflakedb/snowflake-vscode-extension/pull/550

TODO:

  • get some help writing integration tests to help prevent CLI changes breaking VS Code extension for Snowflake

@sfc-gh-klin sfc-gh-klin requested review from a team as code owners June 18, 2024 17:58
@sfc-gh-klin sfc-gh-klin changed the title [APPS-41865] Add LSP plugin interface with pygls language server to allow communication between vs code extension [APPS-41865] Add LSP plugin interface with pygls language server to formalize communication with vs code extension Jun 18, 2024
Comment on lines 46 to 28
capabilities={
"createApplication": True,
"openApplication": True,
"teardownApplication": True,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an enum? We are duplicating the naming here and in command decorators

def __init__(self):
super().__init__()
server = LanguageServer(
name="lsp_controller", version="v0.0.1", max_workers=MAX_WORKERS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really worried in how many places we have version strings.

Copy link
Author

@sfc-gh-klin sfc-gh-klin Jul 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I changed it to have a single version number for the lsp plugin, since that's what from pygls gets passed into ServerInfo to the client during init.

plugin_func(server, context)
server.start_io()

def discover_lsp_plugins(self, package_name):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need security consultation for risk of importing 3rd party modules.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if I am only looking to import the internal ones? I don't see a need for the external packages.

Copy link
Contributor

@sfc-gh-turbaszek sfc-gh-turbaszek Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this guaranteed somewhere? Are we sure 3rd party plugins can't be loaded?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I am doing it now follows the path of calling load_only_builtin_command_plugins

def load_only_builtin_command_plugins() -> List[LoadedCommandPlugin]:
    loader = CommandPluginsLoader()
    loader.register_builtin_plugins()
    return loader.load_all_registered_plugins()

"get_builtin_plugin_name_to_plugin_spec" so it's only the stuff in builtin_plugins.py, which restricts it to the internal ones.

See load_lsp_plugins for where it calls the load_only_builtin_command_plugins.

Copy link
Contributor

@sfc-gh-turbaszek sfc-gh-turbaszek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left few comments, I'm mostly worried about how much code duplication we have at this moment.

@sfc-gh-klin sfc-gh-klin force-pushed the klin-lsp branch 2 times, most recently from 5a75f00 to ccf39fb Compare July 1, 2024 17:42
Comment on lines 48 to 62
plugins = []
package = importlib.import_module(package_name)
for _, module_name, is_pkg in pkgutil.iter_modules(package.__path__):
if is_pkg:
submodule = importlib.import_module(f"{package_name}.{module_name}")
for _, submodule_name, is_pkg in pkgutil.iter_modules(
submodule.__path__
):
module = importlib.import_module(
f"{package_name}.{module_name}.{submodule_name}"
)
for _name, obj in inspect.getmembers(module, inspect.isfunction):
if hasattr(obj, "_lsp_context"):
plugins.append(obj)
return plugins
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sfc-gh-turbaszek I've tried to use Pluggy to replace this discovery mechanism and I am stumped. I tried following a small toy example of it from their docs but I don't understand how the automatic plugin discovery is supposed to work, because it seems like it either needs some mapping the way the command_plugins_loader.py has get_builtin_plugin_name, or it uses the setuptools_entrypoints which I don't think I should need since there's no need for external plugins here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like SnowCLI uses the hook markers but then uses its own mechanism to identify the plugins. The LoadedCommandPlugin has a command_spec that gets used in "_load_command_spec" in command_plugins_loader to return the plugin.command_spec(), and the contents of that occur in plugin_spec.py for each plugin. The command_spec() for native apps for example is:

@plugin_hook_impl
def command_spec():
    return CommandSpec(
        parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
        command_type=CommandType.COMMAND_GROUP,
        typer_instance=commands.app.create_instance(),
    )

And it seems like the @plugin_hook_impl from the pluggy marker is not really used/needed by the builtin plugin process since the code doesn't use pluggy for discovery of those builtins. e.g., removing @plugin_hook_impl, everything still works.

So I guess I can follow the pattern there for command_spec but with another spec for the LSP stuff.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the LSP is only internal thing, and we don't expect plugins as packages why we are using pluggy? Do we really need automatic discovery? I think many things will become easier once we start implementing entities framework.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using pluggy seemed more official and in line with the code in the repo than the way I was doing it before. Did you prefer the previous version that didn't use pluggy? In the end SnowCLI has its own mechanism to find the plugins; the plugin.command_spec and plugin.lsp_spec, so it isn't automatic discovery for the internal plugins. That's my current understanding.

return MessageResult(f"{url}")
else:
return MessageResult(
'Snowflake Native App not yet deployed! Please run "snow app run" first.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a matter of style, it would make sense to direct clients to run a different server_command rather than using CLI terminology here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it'd be good to have unique error names or error codes in these MessageResults that are given to the VS Code extension?

@sfc-gh-turbaszek
Copy link
Contributor

@sfc-gh-klin what's the plan for end to end testing of LSP plugin? How we make sure that cli changes won't break VSC?

@sfc-gh-klin sfc-gh-klin force-pushed the klin-lsp branch 2 times, most recently from b61b47d to f459eab Compare July 16, 2024 17:45
@sfc-gh-klin
Copy link
Author

@sfc-gh-klin what's the plan for end to end testing of LSP plugin? How we make sure that cli changes won't break VSC?

I think having integration tests that start up the LSP server and then tests the interface would help ensure CLI changes don't break the extension. I'm not too well versed with writing integration tests in Python though so any pointers would be appreciated.

@sfc-gh-klin
Copy link
Author

I tried to set up integration tests but my test is hanging and I'm trying to debug why. Do you guys have tips for how to get it to log some information when running? I'm running with python -m hatch to run my one integration test and naively trying to add repr() or logging.getLogger(name) and trying to log information doesn't work through that method.

@sfc-gh-klin
Copy link
Author

🫡

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.

4 participants