Commit 8d605491 authored by Günther Wagner's avatar Günther Wagner Committed by Christian Hergert
Browse files

rust-analyzer: added plugin for LSP client

parent 9830cb54
......@@ -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')
......
......@@ -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')),
......
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
[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@
#!/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)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment