[gnome-builder] rust-analyzer: added plugin for LSP client
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder] rust-analyzer: added plugin for LSP client
- Date: Mon, 11 May 2020 23:41:43 +0000 (UTC)
commit 8d605491131ca55934850bd4b221c7e1a3f9085d
Author: Günther Wagner <info gunibert de>
Date: Sat Apr 25 20:10:45 2020 +0200
rust-analyzer: added plugin for LSP client
meson_options.txt | 3 +-
src/plugins/meson.build | 2 +
src/plugins/rust-analyzer/meson.build | 13 ++
src/plugins/rust-analyzer/rust-analyzer.plugin | 16 ++
src/plugins/rust-analyzer/rust_analyzer_plugin.py | 254 ++++++++++++++++++++++
5 files changed, 287 insertions(+), 1 deletion(-)
---
diff --git a/meson_options.txt b/meson_options.txt
index 496fbb6f1..f187c821f 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -64,7 +64,8 @@ option('plugin_python_pack', type: 'boolean')
option('plugin_qemu', type: 'boolean')
option('plugin_quick_highlight', type: 'boolean')
option('plugin_retab', type: 'boolean')
-option('plugin_rls', type: 'boolean')
+option('plugin_rls', type: 'boolean', value: false)
+option('plugin_rust_analyzer', type: 'boolean')
option('plugin_rustup', type: 'boolean')
option('plugin_shellcmd', type: 'boolean')
option('plugin_spellcheck', type: 'boolean')
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index 5690b5ffa..bee20b6fd 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -106,6 +106,7 @@ subdir('recent')
subdir('restore-cursor')
subdir('retab')
subdir('rls')
+subdir('rust-analyzer')
subdir('rustup')
subdir('shellcmd')
subdir('snippets')
@@ -185,6 +186,7 @@ status += [
'Quick Highlight ....... : @0@'.format(get_option('plugin_quick_highlight')),
'Retab ................. : @0@'.format(get_option('plugin_retab')),
'RLS ................... : @0@'.format(get_option('plugin_rls')),
+ 'Rust Analyzer ......... : @0@'.format(get_option('plugin_rust_analyzer')),
'Rustup ................ : @0@'.format(get_option('plugin_rustup')),
'Spellcheck ............ : @0@'.format(get_option('plugin_spellcheck')),
'Stylelint ............. : @0@'.format(get_option('plugin_stylelint')),
diff --git a/src/plugins/rust-analyzer/meson.build b/src/plugins/rust-analyzer/meson.build
new file mode 100644
index 000000000..3d7097d3c
--- /dev/null
+++ b/src/plugins/rust-analyzer/meson.build
@@ -0,0 +1,13 @@
+if get_option('plugin_rust_analyzer')
+
+install_data('rust_analyzer_plugin.py', install_dir: plugindir)
+
+configure_file(
+ input: 'rust-analyzer.plugin',
+ output: 'rust-analyzer.plugin',
+ configuration: config_h,
+ install: true,
+ install_dir: plugindir,
+)
+
+endif
diff --git a/src/plugins/rust-analyzer/rust-analyzer.plugin b/src/plugins/rust-analyzer/rust-analyzer.plugin
new file mode 100644
index 000000000..231d28819
--- /dev/null
+++ b/src/plugins/rust-analyzer/rust-analyzer.plugin
@@ -0,0 +1,16 @@
+[Plugin]
+Authors=Günther Wagner <info gunibert de>
+Builtin=true
+Copyright=Copyright © 2020 Günther Wagner
+Description=Provides auto-completion, diagnostics, and other IDE features
+Loader=python3
+Module=rust_analyzer_plugin
+Name=Rust Analyzer Language Server Integration
+X-Completion-Provider-Languages=rust
+X-Diagnostic-Provider-Languages=rust
+X-Formatter-Languages=rust
+X-Highlighter-Languages=rust
+X-Hover-Provider-Languages=rust
+X-Rename-Provider-Languages=rust
+X-Symbol-Resolver-Languages=rust
+X-Builder-ABI=@PACKAGE_ABI@
diff --git a/src/plugins/rust-analyzer/rust_analyzer_plugin.py
b/src/plugins/rust-analyzer/rust_analyzer_plugin.py
new file mode 100644
index 000000000..93dff63e2
--- /dev/null
+++ b/src/plugins/rust-analyzer/rust_analyzer_plugin.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python
+
+# rust_langserv_plugin.py
+#
+# Copyright 2016 Christian Hergert <chergert redhat com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+This plugin provides integration with the Rust Language Server.
+It builds off the generic language service components in libide
+by bridging them to our supervised Rust Language Server.
+"""
+
+import gi
+import os
+
+from gi.repository import GLib
+from gi.repository import Gio
+from gi.repository import GObject
+from gi.repository import Ide
+
+DEV_MODE = False
+
+class RlsService(Ide.Object):
+ _client = None
+ _has_started = False
+ _supervisor = None
+ _monitor = None
+
+ @classmethod
+ def from_context(klass, context):
+ return context.ensure_child_typed(RlsService)
+
+ @GObject.Property(type=Ide.LspClient)
+ def client(self):
+ return self._client
+
+ @client.setter
+ def client(self, value):
+ self._client = value
+ self.notify('client')
+
+ def do_parent_set(self, parent):
+ """
+ After the context has been loaded, we want to watch the project
+ Cargo.toml for changes if we find one. That will allow us to
+ restart the process as necessary to pick up changes.
+ """
+ if parent is None:
+ return
+
+ context = self.get_context()
+ workdir = context.ref_workdir()
+ cargo_toml = workdir.get_child('Cargo.toml')
+
+ if cargo_toml.query_exists():
+ try:
+ self._monitor = cargo_toml.monitor(0, None)
+ self._monitor.set_rate_limit(5 * 1000) # 5 Seconds
+ self._monitor.connect('changed', self._monitor_changed_cb)
+ except Exception as ex:
+ Ide.debug('Failed to monitor Cargo.toml for changes:', repr(ex))
+
+ def _monitor_changed_cb(self, monitor, file, other_file, event_type):
+ """
+ This method is called when Cargo.toml has changed. We need to
+ cancel any supervised process and force the language server to
+ restart. Otherwise, we risk it not picking up necessary changes.
+ """
+ if self._supervisor is not None:
+ subprocess = self._supervisor.get_subprocess()
+ if subprocess is not None:
+ subprocess.force_exit()
+
+ def do_stop(self):
+ """
+ Stops the Rust Language Server upon request to shutdown the
+ RlsService.
+ """
+ if self._monitor is not None:
+ monitor, self._monitor = self._monitor, None
+ if monitor is not None:
+ monitor.cancel()
+
+ if self._supervisor is not None:
+ supervisor, self._supervisor = self._supervisor, None
+ supervisor.stop()
+
+ def _ensure_started(self):
+ """
+ Start the rust service which provides communication with the
+ Rust Analyzer Language Server. We supervise our own instance of the
+ language server and restart it as necessary using the
+ Ide.SubprocessSupervisor.
+
+ Various extension points (diagnostics, symbol providers, etc) use
+ the RlsService to access the rust components they need.
+ """
+ # To avoid starting the `rust-analyzer-linux` process unconditionally at startup,
+ # we lazily start it when the first provider tries to bind a client
+ # to its :client property.
+ if not self._has_started:
+ self._has_started = True
+
+ # Setup a launcher to spawn the rust language server
+ launcher = self._create_launcher()
+ launcher.set_clear_env(False)
+ sysroot = self._discover_sysroot()
+ if sysroot:
+ launcher.setenv("SYS_ROOT", sysroot, True)
+ launcher.setenv("LD_LIBRARY_PATH", os.path.join(sysroot, "lib"), True)
+ if DEV_MODE:
+ launcher.setenv('RUST_LOG', 'debug', True)
+
+ # Locate the directory of the project and run rls from there.
+ workdir = self.get_context().ref_workdir()
+ launcher.set_cwd(workdir.get_path())
+
+ # If rls was installed with Cargo, try to discover that
+ # to save the user having to update PATH.
+ path_to_rust_analyzer_bin = os.path.expanduser("~/.cargo/bin/rust-analyzer-linux")
+ if os.path.exists(path_to_rust_analyzer_bin):
+ old_path = os.getenv('PATH')
+ new_path = os.path.expanduser('~/.cargo/bin')
+ if old_path is not None:
+ new_path += os.path.pathsep + old_path
+ launcher.setenv('PATH', new_path, True)
+ else:
+ path_to_rls = "rust-analyzer-linux"
+
+ # Setup our Argv. We want to communicate over STDIN/STDOUT,
+ # so it does not require any command line options.
+ launcher.push_argv(path_to_rls)
+
+ # Spawn our peer process and monitor it for
+ # crashes. We may need to restart it occasionally.
+ self._supervisor = Ide.SubprocessSupervisor()
+ self._supervisor.connect('spawned', self._rls_spawned)
+ self._supervisor.set_launcher(launcher)
+ self._supervisor.start()
+
+ def _rls_spawned(self, supervisor, subprocess):
+ """
+ This callback is executed when the `rls` process is spawned.
+ We can use the stdin/stdout to create a channel for our
+ LspClient.
+ """
+ stdin = subprocess.get_stdin_pipe()
+ stdout = subprocess.get_stdout_pipe()
+ io_stream = Gio.SimpleIOStream.new(stdout, stdin)
+
+ if self._client:
+ self._client.stop()
+ self._client.destroy()
+
+ self._client = Ide.LspClient.new(io_stream)
+ self.append(self._client)
+ self._client.add_language('rust')
+ self._client.start()
+ self.notify('client')
+
+ def _create_launcher(self):
+ """
+ Creates a launcher to be used by the rust service. This needs
+ to be run on the host because we do not currently bundle rust
+ inside our flatpak.
+
+ In the future, we might be able to rely on the runtime for
+ the tooling. Maybe even the program if flatpak-builder has
+ prebuilt our dependencies.
+ """
+ flags = Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE
+ if not DEV_MODE:
+ flags |= Gio.SubprocessFlags.STDERR_SILENCE
+ launcher = Ide.SubprocessLauncher()
+ launcher.set_flags(flags)
+ launcher.set_cwd(GLib.get_home_dir())
+ launcher.set_run_on_host(True)
+ return launcher
+
+ def _discover_sysroot(self):
+ """
+ The Rust Language Server needs to know where the sysroot is of
+ the Rust installation we are using. This is simple enough to
+ get, by using `rust --print sysroot` as the rust-language-server
+ documentation suggests.
+ """
+ for rustc in ['rustc', os.path.expanduser('~/.cargo/bin/rustc')]:
+ try:
+ launcher = self._create_launcher()
+ launcher.push_args([rustc, '--print', 'sysroot'])
+ subprocess = launcher.spawn()
+ _, stdout, _ = subprocess.communicate_utf8()
+ return stdout.strip()
+ except:
+ pass
+
+ @classmethod
+ def bind_client(klass, provider):
+ """
+ This helper tracks changes to our client as it might happen when
+ our `rls` process has crashed.
+ """
+ context = provider.get_context()
+ self = RlsService.from_context(context)
+ self._ensure_started()
+ self.bind_property('client', provider, 'client', GObject.BindingFlags.SYNC_CREATE)
+
+class RlsDiagnosticProvider(Ide.LspDiagnosticProvider, Ide.DiagnosticProvider):
+ def do_load(self):
+ RlsService.bind_client(self)
+
+class RlsCompletionProvider(Ide.LspCompletionProvider, Ide.CompletionProvider):
+ def do_load(self, context):
+ RlsService.bind_client(self)
+
+ def do_get_priority(self, context):
+ # This provider only activates when it is very likely that we
+ # want the results. So use high priority (negative is better).
+ return -1000
+
+class RlsRenameProvider(Ide.LspRenameProvider, Ide.RenameProvider):
+ def do_load(self):
+ RlsService.bind_client(self)
+
+class RlsSymbolResolver(Ide.LspSymbolResolver, Ide.SymbolResolver):
+ def do_load(self):
+ RlsService.bind_client(self)
+
+class RlsHighlighter(Ide.LspHighlighter, Ide.Highlighter):
+ def do_load(self):
+ RlsService.bind_client(self)
+
+class RlsFormatter(Ide.LspFormatter, Ide.Formatter):
+ def do_load(self):
+ RlsService.bind_client(self)
+
+class RlsHoverProvider(Ide.LspHoverProvider, Ide.HoverProvider):
+ def do_prepare(self):
+ self.props.category = 'Rust'
+ self.props.priority = 200
+ RlsService.bind_client(self)
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]