gem-graph-client/libide/lsp/ide-lsp-service.c

791 lines
23 KiB
C

/* ide-lsp-service.c
*
* Copyright 2021 James Westman <james@jwestman.net>
*
* 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/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define G_LOG_DOMAIN "ide-lsp-service"
#include "config.h"
#include "ide-lsp-service.h"
/**
* SECTION:ide-lsp-service
* @title: IdeLspService
* @short_description: Service integration for LSPs
*
* Since: 42.0
*/
typedef struct
{
IdeSubprocessSupervisor *supervisor;
IdeLspClient *client;
char *program;
char **search_path;
guint has_started : 1;
guint inherit_stderr : 1;
} IdeLspServicePrivate;
G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (IdeLspService, ide_lsp_service, IDE_TYPE_OBJECT)
enum {
PROP_0,
PROP_CLIENT,
PROP_INHERIT_STDERR,
PROP_PROGRAM,
PROP_SEARCH_PATH,
PROP_SUPERVISOR,
N_PROPS
};
enum {
CREATE_LAUNCHER,
N_SIGNALS
};
static GParamSpec *properties [N_PROPS];
static guint signals [N_SIGNALS];
static void
ide_lsp_service_stop (IdeLspService *self)
{
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
gboolean notify_client = FALSE;
gboolean notify_supervisor = FALSE;
IDE_ENTRY;
g_return_if_fail (IDE_IS_MAIN_THREAD ());
g_return_if_fail (IDE_IS_LSP_SERVICE (self));
if (priv->has_started)
g_debug ("Stopping LSP client %s", G_OBJECT_TYPE_NAME (self));
if (priv->client != NULL)
{
ide_lsp_client_stop (priv->client);
ide_object_destroy (IDE_OBJECT (priv->client));
priv->client = NULL;
notify_client = TRUE;
}
if (priv->supervisor != NULL)
{
ide_subprocess_supervisor_stop (priv->supervisor);
g_clear_object (&priv->supervisor);
notify_supervisor = TRUE;
}
priv->has_started = FALSE;
if (notify_client)
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CLIENT]);
if (notify_supervisor)
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SUPERVISOR]);
IDE_EXIT;
}
static void
ide_lsp_service_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
IdeLspService *self = IDE_LSP_SERVICE (object);
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
switch (prop_id)
{
case PROP_CLIENT:
g_value_set_object (value, priv->client);
break;
case PROP_PROGRAM:
g_value_set_string (value, priv->program);
break;
case PROP_SEARCH_PATH:
g_value_set_boxed (value, priv->search_path);
break;
case PROP_SUPERVISOR:
g_value_set_object (value, priv->supervisor);
break;
case PROP_INHERIT_STDERR:
g_value_set_boolean (value, priv->inherit_stderr);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
ide_lsp_service_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
IdeLspService *self = IDE_LSP_SERVICE (object);
switch (prop_id)
{
case PROP_INHERIT_STDERR:
ide_lsp_service_set_inherit_stderr (self, g_value_get_boolean (value));
break;
case PROP_PROGRAM:
ide_lsp_service_set_program (self, g_value_get_string (value));
break;
case PROP_SEARCH_PATH:
ide_lsp_service_set_search_path (self, g_value_get_boxed (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
ide_lsp_service_destroy (IdeObject *object)
{
IdeLspService *self = (IdeLspService *)object;
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
IDE_ENTRY;
ide_lsp_service_stop (self);
g_clear_object (&priv->supervisor);
g_clear_object (&priv->client);
g_clear_pointer (&priv->program, g_free);
g_clear_pointer (&priv->search_path, g_strfreev);
IDE_OBJECT_CLASS (ide_lsp_service_parent_class)->destroy (object);
IDE_EXIT;
}
static IdeSubprocessLauncher *
ide_lsp_service_real_create_launcher (IdeLspService *self,
IdePipeline *pipeline,
GSubprocessFlags flags)
{
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
g_autoptr(IdeSubprocessLauncher) launcher = NULL;
g_autoptr(IdeContext) context = NULL;
const char *srcdir;
IDE_ENTRY;
g_assert (IDE_IS_LSP_SERVICE (self));
g_assert (IDE_IS_PIPELINE (pipeline));
if (priv->program == NULL)
IDE_RETURN (NULL);
context = ide_object_ref_context (IDE_OBJECT (self));
srcdir = ide_pipeline_get_srcdir (pipeline);
/* First try in the build environment */
if (ide_pipeline_contains_program_in_path (pipeline, priv->program, NULL))
{
if ((launcher = ide_pipeline_create_launcher (pipeline, NULL)))
{
ide_subprocess_launcher_set_flags (launcher, flags);
ide_subprocess_launcher_push_argv (launcher, priv->program);
ide_subprocess_launcher_set_cwd (launcher, srcdir);
IDE_RETURN (g_steal_pointer (&launcher));
}
}
/* Then try on the host if we find it there */
if (launcher == NULL)
{
IdeRuntimeManager *runtime_manager = ide_runtime_manager_from_context (context);
IdeRuntime *host = ide_runtime_manager_get_runtime (runtime_manager, "host");
if (ide_runtime_contains_program_in_path (host, priv->program, NULL))
{
if ((launcher = ide_runtime_create_launcher (host, NULL)))
{
ide_subprocess_launcher_set_flags (launcher, flags);
ide_subprocess_launcher_push_argv (launcher, priv->program);
ide_subprocess_launcher_set_cwd (launcher, srcdir);
IDE_RETURN (g_steal_pointer (&launcher));
}
}
/* If we didn't find it in the host, we might have an alternate
* search path we can try.
*/
if (priv->search_path)
{
for (guint i = 0; priv->search_path[i]; i++)
{
g_autofree char *path = g_build_filename (priv->search_path[i], priv->program, NULL);
if (g_file_test (path, G_FILE_TEST_IS_EXECUTABLE))
{
if ((launcher = ide_runtime_create_launcher (host, NULL)))
{
ide_subprocess_launcher_push_argv (launcher, path);
ide_subprocess_launcher_set_flags (launcher, flags);
ide_subprocess_launcher_set_cwd (launcher, srcdir);
IDE_RETURN (g_steal_pointer (&launcher));
}
}
}
}
}
/* Finally fallback to Builder's execution runtime */
if (launcher == NULL)
{
g_autofree char *path = NULL;
if ((path = g_find_program_in_path (priv->program)))
{
launcher = ide_subprocess_launcher_new (flags);
ide_subprocess_launcher_push_argv (launcher, path);
ide_subprocess_launcher_set_cwd (launcher, srcdir);
IDE_RETURN (g_steal_pointer (&launcher));
}
}
IDE_RETURN (NULL);
}
G_NORETURN static void
ide_lsp_service_real_configure_client (IdeLspService *self,
IdeLspClient *client)
{
g_assert (IDE_IS_LSP_SERVICE (self));
g_assert (IDE_IS_LSP_CLIENT (client));
g_assert_not_reached ();
}
static void
ide_lsp_service_real_configure_launcher (IdeLspService *self,
IdePipeline *pipeline,
IdeSubprocessLauncher *launcher)
{
IDE_ENTRY;
g_assert (IDE_IS_LSP_SERVICE (self));
g_assert (!pipeline || IDE_IS_PIPELINE (pipeline));
g_assert (IDE_IS_SUBPROCESS_LAUNCHER (launcher));
IDE_EXIT;
}
static void
ide_lsp_service_real_configure_supervisor (IdeLspService *self,
IdeSubprocessSupervisor *client)
{
IDE_ENTRY;
IDE_EXIT;
}
static void
ide_lsp_service_class_init (IdeLspServiceClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
IdeObjectClass *ide_object_class = IDE_OBJECT_CLASS (klass);
IdeLspServiceClass *service_class = IDE_LSP_SERVICE_CLASS (klass);
object_class->get_property = ide_lsp_service_get_property;
object_class->set_property = ide_lsp_service_set_property;
ide_object_class->destroy = ide_lsp_service_destroy;
service_class->create_launcher = ide_lsp_service_real_create_launcher;
service_class->configure_client = ide_lsp_service_real_configure_client;
service_class->configure_launcher = ide_lsp_service_real_configure_launcher;
service_class->configure_supervisor = ide_lsp_service_real_configure_supervisor;
/**
* IdeLspService:client:
*
* The [class@LspClient] provided by the service, or %NULL if it has not been started yet.
*/
properties[PROP_CLIENT] =
g_param_spec_object ("client",
"Client",
"Client",
IDE_TYPE_LSP_CLIENT,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
/**
* IdeLspService:program:
*
* The "program" property contains the name of the executable to
* launch. If this is set, the create-launcher signal will use it
* to locate and execute the program if found.
*/
properties [PROP_PROGRAM] =
g_param_spec_string ("program",
"Program",
"The program executable name",
NULL,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
/**
* IdeLspService:search-path:
*
* An alternate search path to locate the program on the host.
*/
properties [PROP_SEARCH_PATH] =
g_param_spec_boxed ("search-path",
"Search Path",
"Search Path",
G_TYPE_STRV,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
/**
* IdeLspService:supervisor:
*
* The [class@SubprocessSupervisor] that manages the language server process, or %NULL if the
* service is not running.
*/
properties[PROP_SUPERVISOR] =
g_param_spec_object ("supervisor",
"Supervisor",
"Supervisor",
IDE_TYPE_LSP_CLIENT,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
/**
* IdeLspService:inherit-stderr:
*
* If inherit-stderr is enabled, the language server process's stderr is passed through to Builder's.
*/
properties[PROP_INHERIT_STDERR] =
g_param_spec_boolean ("inherit-stderr",
"Inherit stderr",
"Inherit stderr",
FALSE,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, N_PROPS, properties);
/**
* IdeLspService::create-launcher:
* @self: an [class@LspService]
* @pipeline: a loaded [class@Pipeline]
* @flags: [flags@Gio.SubprocessFlags] to use for the launcher
*
* Creates the launcher to be used for the LSP.
*
* If you want to use a launcher on the host, this would be a good
* place to determine that.
*
* Returns: (transfer full) (nullable): a [class@SubprocessLauncher] or %NULL
*/
signals [CREATE_LAUNCHER] =
g_signal_new ("create-launcher",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (IdeLspServiceClass, create_launcher),
g_signal_accumulator_first_wins, NULL,
NULL,
IDE_TYPE_SUBPROCESS_LAUNCHER,
2,
IDE_TYPE_PIPELINE,
G_TYPE_SUBPROCESS_FLAGS);
}
static void
ide_lsp_service_init (IdeLspService *self)
{
}
/**
* ide_lsp_service_get_inherit_stderr:
* @self: a [class@LspService]
*
* Gets whether the language server process's stderr output should be passed to Builder's.
*
* Returns: %TRUE if the subprocess inherits stderr, otherwise %FALSE
*/
gboolean
ide_lsp_service_get_inherit_stderr (IdeLspService *self)
{
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
g_return_val_if_fail (IDE_IS_LSP_SERVICE (self), FALSE);
return priv->inherit_stderr;
}
/**
* ide_lsp_service_set_inherit_stderr:
* @self: a [class@LspService]
* @inherit_stderr: %TRUE to enable stderr, %FALSE to disable it
*
* Gets whether the language server process's stderr output should be passed to Builder's.
*/
void
ide_lsp_service_set_inherit_stderr (IdeLspService *self,
gboolean inherit_stderr)
{
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
IDE_ENTRY;
g_return_if_fail (IDE_IS_MAIN_THREAD ());
g_return_if_fail (IDE_IS_LSP_SERVICE (self));
inherit_stderr = !!inherit_stderr;
if (priv->inherit_stderr != inherit_stderr)
{
priv->inherit_stderr = inherit_stderr;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_INHERIT_STDERR]);
}
IDE_EXIT;
}
static void
on_supervisor_spawned_cb (IdeLspService *self,
IdeSubprocess *subprocess,
IdeSubprocessSupervisor *supervisor)
{
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
IdeLspServiceClass *klass;
g_autoptr(GIOStream) iostream = NULL;
g_autoptr(IdeLspClient) client = NULL;
g_autoptr(GInputStream) to_stdout = NULL;
g_autoptr(GOutputStream) to_stdin = NULL;
IDE_ENTRY;
g_assert (IDE_IS_MAIN_THREAD ());
g_assert (IDE_IS_LSP_SERVICE (self));
g_assert (IDE_IS_SUBPROCESS (subprocess));
g_assert (IDE_IS_SUBPROCESS_SUPERVISOR (supervisor));
klass = IDE_LSP_SERVICE_GET_CLASS (self);
to_stdin = ide_subprocess_get_stdin_pipe (subprocess);
to_stdout = ide_subprocess_get_stdout_pipe (subprocess);
iostream = g_simple_io_stream_new (to_stdout, to_stdin);
if (priv->client != NULL)
{
ide_lsp_client_stop (priv->client);
ide_object_destroy (IDE_OBJECT (priv->client));
}
client = ide_lsp_client_new (iostream);
ide_object_append (IDE_OBJECT (self), IDE_OBJECT (client));
klass->configure_client (self, client);
ide_lsp_client_start (client);
priv->client = g_steal_pointer (&client);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CLIENT]);
IDE_EXIT;
}
static void
ensure_started (IdeLspService *self,
IdeContext *context)
{
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
g_autoptr(IdeSubprocessLauncher) launcher = NULL;
g_autoptr(IdeSubprocessSupervisor) supervisor = NULL;
IdeBuildManager *build_manager;
IdeLspServiceClass *klass;
IdePipeline *pipeline = NULL;
GSubprocessFlags flags;
IDE_ENTRY;
g_assert (IDE_IS_MAIN_THREAD ());
g_assert (IDE_IS_LSP_SERVICE (self));
g_assert (IDE_IS_CONTEXT (context));
if (priv->has_started)
IDE_EXIT;
g_assert (priv->supervisor == NULL);
g_assert (priv->client == NULL);
klass = IDE_LSP_SERVICE_GET_CLASS (self);
build_manager = ide_build_manager_from_context (context);
pipeline = ide_build_manager_get_pipeline (build_manager);
/* Delay until pipeline is ready */
if (!ide_pipeline_is_ready (pipeline))
IDE_EXIT;
flags = G_SUBPROCESS_FLAGS_STDIN_PIPE | G_SUBPROCESS_FLAGS_STDOUT_PIPE;
if (!priv->inherit_stderr)
flags |= G_SUBPROCESS_FLAGS_STDERR_SILENCE;
/* Allow subclasses to control launcher creation */
g_signal_emit (self, signals [CREATE_LAUNCHER], 0, pipeline, flags, &launcher);
if (launcher == NULL)
IDE_EXIT;
klass->configure_launcher (self, pipeline, launcher);
supervisor = ide_subprocess_supervisor_new ();
ide_subprocess_supervisor_set_launcher (supervisor, launcher);
g_signal_connect_object (supervisor,
"spawned",
G_CALLBACK (on_supervisor_spawned_cb),
self,
G_CONNECT_SWAPPED);
priv->has_started = TRUE;
klass->configure_supervisor (self, supervisor);
ide_subprocess_supervisor_start (supervisor);
priv->supervisor = g_steal_pointer (&supervisor);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SUPERVISOR]);
IDE_EXIT;
}
/**
* ide_lsp_service_restart:
* @self: a [class@LspService]
*
* Restarts the service and its associated process.
*/
void
ide_lsp_service_restart (IdeLspService *self)
{
IdeContext *context;
IDE_ENTRY;
g_return_if_fail (IDE_IS_MAIN_THREAD ());
g_return_if_fail (IDE_IS_LSP_SERVICE (self));
g_return_if_fail (!ide_object_in_destruction (IDE_OBJECT (self)));
ide_lsp_service_stop (self);
if ((context = ide_object_get_context (IDE_OBJECT (self))))
ensure_started (self, context);
IDE_EXIT;
}
static void
on_pipeline_loaded_cb (IdeLspService *self,
IdePipeline *pipeline)
{
IDE_ENTRY;
g_assert (IDE_IS_LSP_SERVICE (self));
g_assert (IDE_IS_PIPELINE (pipeline));
ide_lsp_service_restart (self);
IDE_EXIT;
}
static void
on_notify_pipeline_cb (IdeLspService *self,
GParamSpec *pspec,
IdeBuildManager *build_manager)
{
IdePipeline *pipeline;
IDE_ENTRY;
g_assert (IDE_IS_LSP_SERVICE (self));
g_assert (IDE_IS_BUILD_MANAGER (build_manager));
ide_lsp_service_stop (self);
if ((pipeline = ide_build_manager_get_pipeline (build_manager)))
{
if (!ide_pipeline_is_ready (pipeline))
g_signal_connect_object (pipeline,
"loaded",
G_CALLBACK (on_pipeline_loaded_cb),
self,
G_CONNECT_SWAPPED);
else
ide_lsp_service_restart (self);
}
IDE_EXIT;
}
static void
ide_lsp_service_class_bind_client_internal (IdeLspServiceClass *klass,
IdeObject *provider,
gboolean autostart)
{
IdeContext *context;
GParamSpec *pspec;
IDE_ENTRY;
g_return_if_fail (IDE_IS_MAIN_THREAD ());
g_return_if_fail (IDE_IS_LSP_SERVICE_CLASS (klass));
g_return_if_fail (IDE_IS_OBJECT (provider));
pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (provider), "client");
g_return_if_fail (pspec != NULL && g_type_is_a (pspec->value_type, IDE_TYPE_LSP_CLIENT));
context = ide_object_get_context (provider);
g_return_if_fail (IDE_IS_CONTEXT (context));
/* If the context has a project (ie: not editor mode), then we
* want to track changes to the pipeline so we can reload the
* language server automatically.
*/
if (ide_context_has_project (context))
{
IdeLspServicePrivate *priv;
IdeBuildManager *build_manager = ide_build_manager_from_context (context);
g_autoptr(IdeLspService) service = NULL;
gboolean do_notify = FALSE;
if (!(service = ide_object_get_child_typed (IDE_OBJECT (context), G_OBJECT_CLASS_TYPE (klass))))
{
service = ide_object_ensure_child_typed (IDE_OBJECT (context), G_OBJECT_CLASS_TYPE (klass));
g_signal_connect_object (build_manager,
"notify::pipeline",
G_CALLBACK (on_notify_pipeline_cb),
service,
G_CONNECT_SWAPPED);
do_notify = TRUE;
}
priv = ide_lsp_service_get_instance_private (service);
do_notify |= (autostart && !priv->has_started);
if (do_notify)
on_notify_pipeline_cb (service, NULL, build_manager);
g_object_bind_property (service, "client", provider, "client", G_BINDING_SYNC_CREATE);
}
IDE_EXIT;
}
/**
* ide_lsp_service_class_bind_client:
* @klass: a [class@LspService] class structure
* @provider: an [class@Object]
*
* Binds the "client" property of @property to its context's instance of
* @klass. If the language server is not running yet, it will be started.
*/
void
ide_lsp_service_class_bind_client (IdeLspServiceClass *klass,
IdeObject *provider)
{
ide_lsp_service_class_bind_client_internal (klass, provider, TRUE);
}
/**
* ide_lsp_service_class_bind_client_lazy:
* @klass: a [class@LspService] class structure
* @provider: an [class@Object]
*
* Like ide_lsp_service_bind_client() but will not immediately spawn
* the language server.
*/
void
ide_lsp_service_class_bind_client_lazy (IdeLspServiceClass *klass,
IdeObject *provider)
{
ide_lsp_service_class_bind_client_internal (klass, provider, FALSE);
}
const char *
ide_lsp_service_get_program (IdeLspService *self)
{
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
g_return_val_if_fail (IDE_IS_LSP_SERVICE (self), NULL);
return priv->program;
}
void
ide_lsp_service_set_program (IdeLspService *self,
const char *program)
{
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
g_return_if_fail (IDE_IS_LSP_SERVICE (self));
if (g_strcmp0 (program, priv->program) != 0)
{
g_free (priv->program);
priv->program = g_strdup (program);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRAM]);
}
}
const char * const *
ide_lsp_service_get_search_path (IdeLspService *self)
{
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
g_return_val_if_fail (IDE_IS_LSP_SERVICE (self), NULL);
return (const char * const *)priv->search_path;
}
/**
* ide_lsp_service_set_search_path:
* @self: a #IdeLspService
* @search_path: (array zero-terminated=1) (element-type utf8) (nullable):
* a search path to apply when searching the host or %NULL.
*
* Sets an alternate search path to use when discovering programs on
* the host system.
*/
void
ide_lsp_service_set_search_path (IdeLspService *self,
const char * const *search_path)
{
IdeLspServicePrivate *priv = ide_lsp_service_get_instance_private (self);
g_return_if_fail (IDE_IS_LSP_SERVICE (self));
if ((const char * const *)priv->search_path == search_path)
return;
g_strfreev (priv->search_path);
priv->search_path = g_strdupv ((char **)search_path);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SEARCH_PATH]);
}