Loading
  1. #!/usr/bin/env python
  2.  
  3. # rust_langserv_plugin.py
  4. #
  5. # Copyright 2016 Christian Hergert <chergert@redhat.com>
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program.  If not, see <http://www.gnu.org/licenses/>.
  19.  
  20. """
  21. This plugin provides integration with the Rust Language Server.
  22. It builds off the generic language service components in libide
  23. by bridging them to our supervised Rust Language Server.
  24. """
  25.  
  26. import gi
  27. import os
  28.  
  29. from gi.repository import GLib
  30. from gi.repository import Gio
  31. from gi.repository import GObject
  32. from gi.repository import Ide
  33.  
  34. DEV_MODE = False
  35.  
  36. class RlsService(Ide.Object):
  37.     _client = None
  38.     _has_started = False
  39.     _supervisor = None
  40.     _monitor = None
  41.  
  42.     @classmethod
  43.     def from_context(klass, context):
  44.         return context.ensure_child_typed(RlsService)
  45.  
  46.     @GObject.Property(type=Ide.LspClient)
  47.     def client(self):
  48.         return self._client
  49.  
  50.     @client.setter
  51.     def client(self, value):
  52.         self._client = value
  53.         self.notify('client')
  54.  
  55.     def do_parent_set(self, parent):
  56.         """
  57.        After the context has been loaded, we want to watch the project
  58.        Cargo.toml for changes if we find one. That will allow us to
  59.        restart the process as necessary to pick up changes.
  60.        """
  61.         if parent is None:
  62.             return
  63.  
  64.         context = self.get_context()
  65.         workdir = context.ref_workdir()
  66.         cargo_toml = workdir.get_child('Cargo.toml')
  67.  
  68.         if cargo_toml.query_exists():
  69.             try:
  70.                 self._monitor = cargo_toml.monitor(0, None)
  71.                 self._monitor.set_rate_limit(5 * 1000) # 5 Seconds
  72.                 self._monitor.connect('changed', self._monitor_changed_cb)
  73.             except Exception as ex:
  74.                 Ide.debug('Failed to monitor Cargo.toml for changes:', repr(ex))
  75.  
  76.     def _monitor_changed_cb(self, monitor, file, other_file, event_type):
  77.         """
  78.        This method is called when Cargo.toml has changed. We need to
  79.        cancel any supervised process and force the language server to
  80.        restart. Otherwise, we risk it not picking up necessary changes.
  81.        """
  82.         if self._supervisor is not None:
  83.             subprocess = self._supervisor.get_subprocess()
  84.             if subprocess is not None:
  85.                 subprocess.force_exit()
  86.  
  87.     def do_stop(self):
  88.         """
  89.        Stops the Rust Language Server upon request to shutdown the
  90.        RlsService.
  91.        """
  92.         if self._monitor is not None:
  93.             monitor, self._monitor = self._monitor, None
  94.             if monitor is not None:
  95.                 monitor.cancel()
  96.  
  97.         if self._supervisor is not None:
  98.             supervisor, self._supervisor = self._supervisor, None
  99.             supervisor.stop()
  100.  
  101.     def _ensure_started(self):
  102.         """
  103.        Start the rust service which provides communication with the
  104.        Rust Language Server. We supervise our own instance of the
  105.        language server and restart it as necessary using the
  106.        Ide.SubprocessSupervisor.
  107.  
  108.        Various extension points (diagnostics, symbol providers, etc) use
  109.        the RlsService to access the rust components they need.
  110.        """
  111.         # To avoid starting the `rls` process unconditionally at startup,
  112.         # we lazily start it when the first provider tries to bind a client
  113.         # to its :client property.
  114.         if not self._has_started:
  115.             self._has_started = True
  116.  
  117.             # Setup a launcher to spawn the rust language server
  118.             launcher = self._create_launcher()
  119.             launcher.set_clear_env(False)
  120.             if DEV_MODE:
  121.                 launcher.setenv('RUST_LOG', 'debug', True)
  122.  
  123.             workdir = self.get_context().ref_workdir()
  124.             launcher.set_cwd(workdir.get_path())
  125.  
  126.             # Locate the directory of the project and run rls from there.
  127.             # If rls was installed with Cargo, try to discover that
  128.             # to save the user having to update PATH.
  129.  
  130.             build_manager = Ide.BuildManager.from_context(self.get_context())
  131.             pipeline = build_manager.props.pipeline
  132.             if pipeline.contains_program_in_path('rls', None):
  133.                 path_to_rls = "rls"
  134.             elif os.path.exists(os.path.expanduser("~/.cargo/bin/rls")):
  135.                 old_path = os.getenv('PATH')
  136.                 new_path = os.path.expanduser('~/.cargo/bin')
  137.                 if old_path is not None:
  138.                     new_path += os.path.pathsep + old_path
  139.                 launcher.setenv('PATH', new_path, True)
  140.             # Look for sysroot after setting the right path
  141.  
  142.             sysroot = self._discover_sysroot()
  143.             print("Sysroot ##" + str(sysroot))
  144.             if sysroot:
  145.                 launcher.setenv("SYS_ROOT", sysroot, True)
  146.                 launcher.setenv("LD_LIBRARY_PATH", os.path.join(sysroot, "lib"), True)
  147.  
  148.             # Setup our Argv. We want to communicate over STDIN/STDOUT,
  149.             # so it does not require any command line options.
  150.             launcher.push_argv(path_to_rls)
  151.             # Spawn our peer process and monitor it for
  152.             # crashes. We may need to restart it occasionally.
  153.             self._supervisor = Ide.SubprocessSupervisor()
  154.             self._supervisor.connect('spawned', self._rls_spawned)
  155.             self._supervisor.set_launcher(launcher)
  156.             self._supervisor.start()
  157.  
  158.     def _rls_spawned(self, supervisor, subprocess):
  159.         """
  160.        This callback is executed when the `rls` process is spawned.
  161.        We can use the stdin/stdout to create a channel for our
  162.        LspClient.
  163.        """
  164.  
  165.         stdin = subprocess.get_stdin_pipe()
  166.         stdout = subprocess.get_stdout_pipe()
  167.         io_stream = Gio.SimpleIOStream.new(stdout, stdin)
  168.  
  169.         if self._client:
  170.             self._client.stop()
  171.             self._client.destroy()
  172.  
  173.         self._client = Ide.LspClient.new(io_stream)
  174.         self.append(self._client)
  175.         self._client.add_language('rust')
  176.         self._client.start()
  177.         self.notify('client')
  178.  
  179.     def _create_launcher(self):
  180.         """
  181.        Creates a launcher to be used by the rust service. This needs
  182.        to be run on the host because we do not currently bundle rust
  183.        inside our flatpak.
  184.  
  185.        In the future, we might be able to rely on the runtime for
  186.        the tooling. Maybe even the program if flatpak-builder has
  187.        prebuilt our dependencies.
  188.        """
  189.         flags = Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE
  190.         if not DEV_MODE:
  191.             flags |= Gio.SubprocessFlags.STDERR_SILENCE
  192.  
  193.         context = self.get_context()
  194.         build_manager = Ide.BuildManager.from_context(context)
  195.         pipeline = build_manager.props.pipeline
  196.  
  197.         if pipeline.contains_program_in_path('rls', None):
  198.             launcher = pipeline.get_runtime().create_launcher()
  199.  
  200.             old_path = os.getenv('PATH')
  201.             new_path = '/usr/lib/sdk/rust-stable/bin'
  202.             if old_path is not None:
  203.                 new_path += os.path.pathsep + old_path
  204.             launcher.setenv('PATH', new_path, True)
  205.         else:
  206.             launcher = Ide.SubprocessLauncher()
  207.             launcher.set_run_on_host(True)
  208.         launcher.set_flags(flags)
  209.         return launcher
  210.  
  211.     def _discover_sysroot(self):
  212.         """
  213.        The Rust Language Server needs to know where the sysroot is of
  214.        the Rust installation we are using. This is simple enough to
  215.        get, by using `rust --print sysroot` as the rust-language-server
  216.        documentation suggests.
  217.        """
  218.         for rustc in ['rustc', os.path.expanduser('~/.cargo/bin/rustc')]:
  219.             try:
  220.                 launcher = self._create_launcher()
  221.                 launcher.push_args([rustc, '--print', 'sysroot'])
  222.                 subprocess = launcher.spawn()
  223.                 _, stdout, _ = subprocess.communicate_utf8()
  224.                 return stdout.strip()
  225.             except:
  226.                 pass
  227.  
  228.     @classmethod
  229.     def bind_client(klass, provider):
  230.         """
  231.        This helper tracks changes to our client as it might happen when
  232.        our `rls` process has crashed.
  233.        """
  234.         context = provider.get_context()
  235.         self = RlsService.from_context(context)
  236.         self._ensure_started()
  237.         self.bind_property('client', provider, 'client', GObject.BindingFlags.SYNC_CREATE)
  238.  
  239. class RlsDiagnosticProvider(Ide.LspDiagnosticProvider):
  240.     def do_load(self):
  241.         RlsService.bind_client(self)
  242.  
  243. class RlsCompletionProvider(Ide.LspCompletionProvider):
  244.     def do_load(self, context):
  245.         RlsService.bind_client(self)
  246.  
  247.     def do_get_priority(self, context):
  248.         # This provider only activates when it is very likely that we
  249.         # want the results. So use high priority (negative is better).
  250.         return -1000
  251.  
  252. class RlsRenameProvider(Ide.LspRenameProvider):
  253.     def do_load(self):
  254.         RlsService.bind_client(self)
  255.  
  256. class RlsSymbolResolver(Ide.LspSymbolResolver):
  257.     def do_load(self):
  258.         RlsService.bind_client(self)
  259.  
  260. class RlsHighlighter(Ide.LspHighlighter):
  261.     def do_load(self):
  262.         RlsService.bind_client(self)
  263.  
  264. class RlsFormatter(Ide.LspFormatter):
  265.     def do_load(self):
  266.         RlsService.bind_client(self)
  267.  
  268. class RlsHoverProvider(Ide.LspHoverProvider):
  269.     def do_prepare(self):
  270.         self.props.category = 'Rust'
  271.         self.props.priority = 200
  272.         RlsService.bind_client(self)