Serenity Operating System
1/*
2 * Copyright (c) 2020, the SerenityOS developers.
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "LanguageClient.h"
8#include "HackStudio.h"
9#include "ProjectDeclarations.h"
10#include "ToDoEntries.h"
11#include <AK/DeprecatedString.h>
12#include <AK/Vector.h>
13#include <LibGUI/Notification.h>
14
15namespace HackStudio {
16
17void ConnectionToServer::auto_complete_suggestions(Vector<CodeComprehension::AutocompleteResultEntry> const& suggestions)
18{
19 if (!m_current_language_client) {
20 dbgln("Language Server connection has no attached language client");
21 return;
22 }
23 m_current_language_client->provide_autocomplete_suggestions(suggestions);
24}
25
26void ConnectionToServer::declaration_location(CodeComprehension::ProjectLocation const& location)
27{
28 if (!m_current_language_client) {
29 dbgln("Language Server connection has no attached language client");
30 return;
31 }
32 m_current_language_client->declaration_found(location.file, location.line, location.column);
33}
34
35void ConnectionToServer::parameters_hint_result(Vector<DeprecatedString> const& params, int argument_index)
36{
37 if (!m_current_language_client) {
38 dbgln("Language Server connection has no attached language client");
39 return;
40 }
41
42 VERIFY(argument_index >= 0);
43 m_current_language_client->parameters_hint_result(params, static_cast<size_t>(argument_index));
44}
45
46void ConnectionToServer::tokens_info_result(Vector<CodeComprehension::TokenInfo> const& tokens_info)
47{
48 if (!m_current_language_client) {
49 dbgln("Language Server connection has no attached language client");
50 return;
51 }
52 VERIFY(m_current_language_client->on_tokens_info_result);
53 m_current_language_client->on_tokens_info_result(tokens_info);
54}
55
56void ConnectionToServer::die()
57{
58 VERIFY(m_wrapper);
59 // Wrapper destructs us here
60 m_wrapper->on_crash();
61}
62
63void LanguageClient::open_file(DeprecatedString const& path, int fd)
64{
65 if (!m_connection_wrapper.connection())
66 return;
67 m_connection_wrapper.connection()->async_file_opened(path, fd);
68}
69
70void LanguageClient::set_file_content(DeprecatedString const& path, DeprecatedString const& content)
71{
72 if (!m_connection_wrapper.connection())
73 return;
74 m_connection_wrapper.connection()->async_set_file_content(path, content);
75}
76
77void LanguageClient::insert_text(DeprecatedString const& path, DeprecatedString const& text, size_t line, size_t column)
78{
79 if (!m_connection_wrapper.connection())
80 return;
81 m_connection_wrapper.connection()->async_file_edit_insert_text(path, text, line, column);
82}
83
84void LanguageClient::remove_text(DeprecatedString const& path, size_t from_line, size_t from_column, size_t to_line, size_t to_column)
85{
86 if (!m_connection_wrapper.connection())
87 return;
88 m_connection_wrapper.connection()->async_file_edit_remove_text(path, from_line, from_column, to_line, to_column);
89}
90
91void LanguageClient::request_autocomplete(DeprecatedString const& path, size_t cursor_line, size_t cursor_column)
92{
93 if (!m_connection_wrapper.connection())
94 return;
95 set_active_client();
96 m_connection_wrapper.connection()->async_auto_complete_suggestions(CodeComprehension::ProjectLocation { path, cursor_line, cursor_column });
97}
98
99void LanguageClient::provide_autocomplete_suggestions(Vector<CodeComprehension::AutocompleteResultEntry> const& suggestions) const
100{
101 if (on_autocomplete_suggestions)
102 on_autocomplete_suggestions(suggestions);
103
104 // Otherwise, drop it on the floor :shrug:
105}
106
107void LanguageClient::set_active_client()
108{
109 if (!m_connection_wrapper.connection())
110 return;
111 m_connection_wrapper.set_active_client(*this);
112}
113
114bool LanguageClient::is_active_client() const
115{
116 if (!m_connection_wrapper.connection())
117 return false;
118 return m_connection_wrapper.connection()->active_client() == this;
119}
120
121HashMap<DeprecatedString, NonnullOwnPtr<ConnectionToServerWrapper>> ConnectionToServerInstances::s_instance_for_language;
122
123void ConnectionToServer::declarations_in_document(DeprecatedString const& filename, Vector<CodeComprehension::Declaration> const& declarations)
124{
125 ProjectDeclarations::the().set_declared_symbols(filename, declarations);
126}
127
128void ConnectionToServer::todo_entries_in_document(DeprecatedString const& filename, Vector<CodeComprehension::TodoEntry> const& todo_entries)
129{
130 ToDoEntries::the().set_entries(filename, move(todo_entries));
131}
132
133void LanguageClient::search_declaration(DeprecatedString const& path, size_t line, size_t column)
134{
135 if (!m_connection_wrapper.connection())
136 return;
137 set_active_client();
138 m_connection_wrapper.connection()->async_find_declaration(CodeComprehension::ProjectLocation { path, line, column });
139}
140
141void LanguageClient::get_parameters_hint(DeprecatedString const& path, size_t line, size_t column)
142{
143 if (!m_connection_wrapper.connection())
144 return;
145 set_active_client();
146 m_connection_wrapper.connection()->async_get_parameters_hint(CodeComprehension::ProjectLocation { path, line, column });
147}
148
149void LanguageClient::get_tokens_info(DeprecatedString const& filename)
150{
151 if (!m_connection_wrapper.connection())
152 return;
153 VERIFY(is_active_client());
154 m_connection_wrapper.connection()->async_get_tokens_info(filename);
155}
156
157void LanguageClient::declaration_found(DeprecatedString const& file, size_t line, size_t column) const
158{
159 if (!on_declaration_found) {
160 dbgln("on_declaration_found callback is not set");
161 return;
162 }
163 on_declaration_found(file, line, column);
164}
165
166void LanguageClient::parameters_hint_result(Vector<DeprecatedString> const& params, size_t argument_index) const
167{
168 if (!on_function_parameters_hint_result) {
169 dbgln("on_function_parameters_hint_result callback is not set");
170 return;
171 }
172 on_function_parameters_hint_result(params, argument_index);
173}
174
175void ConnectionToServerInstances::set_instance_for_language(DeprecatedString const& language_name, NonnullOwnPtr<ConnectionToServerWrapper>&& connection_wrapper)
176{
177 s_instance_for_language.set(language_name, move(connection_wrapper));
178}
179
180void ConnectionToServerInstances::remove_instance_for_language(DeprecatedString const& language_name)
181{
182 s_instance_for_language.remove(language_name);
183}
184
185ConnectionToServerWrapper* ConnectionToServerInstances::get_instance_wrapper(DeprecatedString const& language_name)
186{
187 if (auto instance = s_instance_for_language.get(language_name); instance.has_value()) {
188 return const_cast<ConnectionToServerWrapper*>(instance.value());
189 }
190 return nullptr;
191}
192
193void ConnectionToServerWrapper::on_crash()
194{
195 using namespace AK::TimeLiterals;
196
197 show_crash_notification();
198 m_connection.clear();
199
200 static constexpr Time max_crash_frequency = 10_sec;
201 if (m_last_crash_timer.is_valid() && m_last_crash_timer.elapsed_time() < max_crash_frequency) {
202 dbgln("LanguageServer crash frequency is too high");
203 m_respawn_allowed = false;
204
205 show_frequent_crashes_notification();
206 } else {
207 m_last_crash_timer.start();
208 try_respawn_connection();
209 }
210}
211void ConnectionToServerWrapper::show_frequent_crashes_notification() const
212{
213 auto notification = GUI::Notification::construct();
214 notification->set_icon(Gfx::Bitmap::load_from_file("/res/icons/32x32/app-hack-studio.png"sv).release_value_but_fixme_should_propagate_errors());
215 notification->set_title("LanguageServer Crashes too much!");
216 notification->set_text("LanguageServer aided features will not be available in this session");
217 notification->show();
218}
219void ConnectionToServerWrapper::show_crash_notification() const
220{
221 auto notification = GUI::Notification::construct();
222 notification->set_icon(Gfx::Bitmap::load_from_file("/res/icons/32x32/app-hack-studio.png"sv).release_value_but_fixme_should_propagate_errors());
223 notification->set_title("Oops!");
224 notification->set_text(DeprecatedString::formatted("LanguageServer has crashed"));
225 notification->show();
226}
227
228ConnectionToServerWrapper::ConnectionToServerWrapper(DeprecatedString const& language_name, Function<NonnullRefPtr<ConnectionToServer>()> connection_creator)
229 : m_language(Syntax::language_from_name(language_name).value())
230 , m_connection_creator(move(connection_creator))
231{
232 create_connection();
233}
234
235void ConnectionToServerWrapper::create_connection()
236{
237 VERIFY(m_connection.is_null());
238 m_connection = m_connection_creator();
239 m_connection->set_wrapper(*this);
240}
241
242ConnectionToServer* ConnectionToServerWrapper::connection()
243{
244 return m_connection.ptr();
245}
246
247void ConnectionToServerWrapper::attach(LanguageClient& client)
248{
249 m_connection->m_current_language_client = &client;
250}
251
252void ConnectionToServerWrapper::detach()
253{
254 m_connection->m_current_language_client.clear();
255}
256
257void ConnectionToServerWrapper::set_active_client(LanguageClient& client)
258{
259 m_connection->m_current_language_client = &client;
260}
261
262void ConnectionToServerWrapper::try_respawn_connection()
263{
264 if (!m_respawn_allowed)
265 return;
266
267 dbgln("Respawning ConnectionToServer");
268 create_connection();
269
270 // After respawning the language-server, we have to send the content of open project files
271 // so the server's FileDB will be up-to-date.
272 for_each_open_file([this](ProjectFile const& file) {
273 if (file.code_document().language() != m_language)
274 return;
275 m_connection->async_set_file_content(file.code_document().file_path(), file.document().text());
276 });
277}
278
279}