diff options
-rw-r--r-- | buffer.c | 4 | ||||
-rw-r--r-- | config.c | 1 | ||||
-rw-r--r-- | development.md | 2 | ||||
-rw-r--r-- | ide-hover.c | 4 | ||||
-rw-r--r-- | lsp.c | 112 | ||||
-rw-r--r-- | lsp.h | 196 | ||||
-rw-r--r-- | main.c | 9 | ||||
-rw-r--r-- | ted-internal.h | 1 | ||||
-rw-r--r-- | ted.c | 25 | ||||
-rw-r--r-- | ted.cfg | 9 | ||||
-rw-r--r-- | ted.h | 3 |
11 files changed, 255 insertions, 111 deletions
@@ -516,7 +516,7 @@ static void buffer_send_lsp_did_open(TextBuffer *buffer, LSP *lsp) { open->document = lsp_document_id(lsp, buffer->path); open->language = buffer_language(buffer); lsp_send_request(lsp, &request); - buffer->lsp_opened_in = lsp->id; + buffer->lsp_opened_in = lsp_get_id(lsp); } LSP *buffer_lsp(TextBuffer *buffer) { @@ -1911,7 +1911,7 @@ static void buffer_send_lsp_did_change(LSP *lsp, TextBuffer *buffer, BufferPos p range.end = buffer_pos_to_lsp_position(buffer, pos_end); const char *document = buffer->path; - if (lsp->capabilities.incremental_sync_support) { + if (lsp_has_incremental_sync_support(lsp)) { LSPRequest request = {.type = LSP_REQUEST_DID_CHANGE}; LSPDocumentChangeEvent change = { .range = range, @@ -149,6 +149,7 @@ static const SettingFloat settings_float[] = { {"cursor-blink-time-off", &settings_zero.cursor_blink_time_off, 0, 1000, true}, {"hover-time", &settings_zero.hover_time, 0, INFINITY, true}, {"ctrl-scroll-adjust-text-size", &settings_zero.ctrl_scroll_adjust_text_size, -10, 10, true}, + {"lsp-delay", &settings_zero.lsp_delay, 0, 100, true}, }; static const SettingString settings_string[] = { {"build-default-command", settings_zero.build_default_command, sizeof settings_zero.build_default_command, true}, diff --git a/development.md b/development.md index fc23ee5..e3e0519 100644 --- a/development.md +++ b/development.md @@ -96,7 +96,7 @@ When you add a source file to ted, make sure you: ## Adding settings -Find the `Settings` struct in `ted.h` and add the new member. +Find the `Settings` struct in `ted-internal.h` and add the new member. Then go to `config.c` and edit the `settings_<type>` array. ## Adding commands diff --git a/ide-hover.c b/ide-hover.c index 59498f7..ff5fb19 100644 --- a/ide-hover.c +++ b/ide-hover.c @@ -90,7 +90,7 @@ void hover_process_lsp_response(Ted *ted, const LSPResponse *response) { if (hover->text // we already have hover text && ( - lsp->id != hover->last_request.lsp // this request is from a different LSP + lsp_get_id(lsp) != hover->last_request.lsp // this request is from a different LSP || !lsp_document_position_eq(response->request.data.hover.position, pos) // this request is for a different position )) { // this is a stale request. ignore it @@ -141,7 +141,7 @@ void hover_frame(Ted *ted, double dt) { LSPDocumentPosition pos={0}; LSP *lsp=0; if (get_hover_position(ted, &pos, &buffer, &lsp)) { - if (lsp->id != hover->last_request.lsp + if (lsp_get_id(lsp) != hover->last_request.lsp || !lsp_document_position_eq(pos, hover->requested_position)) { // refresh hover hover_send_request(ted); @@ -6,6 +6,10 @@ static LSPMutex request_id_mutex; +u32 lsp_get_id(const LSP *lsp) { + return lsp->id; +} + // it's nice to have request IDs be totally unique, including across LSP servers. static LSPRequestID get_request_id(void) { // it's important that this never returns 0, since that's reserved for "no ID" @@ -479,11 +483,13 @@ static bool lsp_receive(LSP *lsp, size_t max_size) { return true; } -// send requests. +/// send requests. +/// +/// returns `false` if we should quit. static bool lsp_send(LSP *lsp) { if (!lsp->initialized) { // don't send anything before the server is initialized. - return false; + return true; } LSPMessage *messages = NULL; @@ -504,22 +510,46 @@ static bool lsp_send(LSP *lsp) { SDL_UnlockMutex(lsp->messages_mutex); - bool quit = false; + bool alive = true; for (size_t i = 0; i < n_messages; ++i) { LSPMessage *m = &messages[i]; - if (quit) { - lsp_message_free(m); - } else { + bool send = alive; + if (send && m->type == LSP_REQUEST + && i + 1 < n_messages + && messages[i + 1].type == LSP_REQUEST) { + const LSPRequest *r = &m->request; + const LSPRequest *next = &messages[i + 1].request; + if (r->type == LSP_REQUEST_DID_CHANGE + && next->type == LSP_REQUEST_DID_CHANGE + && arr_len(r->data.change.changes) == 1 + && arr_len(next->data.change.changes) == 1 + && !r->data.change.changes[0].use_range + && !next->data.change.changes[1].use_range + && r->data.change.document == next->data.change.document) { + // we don't need to send this request, since it's made + // irrelevant by the next request. + // (specifically, they're both full-document-content + // didChange notifications) + // this helps godot's language server a lot + // since it's super slow because it tries to publish diagnostics + // on every change. + send = false; + } + } + + if (send) { write_message(lsp, m); + } else { + lsp_message_free(m); } if (SDL_SemTryWait(lsp->quit_sem) == 0) { - quit = true; + alive = false; } } free(messages); - return quit; + return alive; } @@ -543,10 +573,20 @@ static int lsp_communication_thread(void *data) { initialize.id = get_request_id(); write_request(lsp, &initialize); + const double send_delay = lsp->send_delay; + double last_send = -DBL_MAX; while (1) { - bool quit = lsp_send(lsp); - if (quit) break; - + bool send = true; + if (send_delay > 0) { + double t = time_get_seconds(); + if (t - last_send > send_delay) { + last_send = t; + } else { + send = false; + } + } + if (send && !lsp_send(lsp)) + break; if (!lsp_receive(lsp, (size_t)10<<20)) break; if (SDL_SemWaitTimeout(lsp->quit_sem, 5) == 0) @@ -569,13 +609,11 @@ static int lsp_communication_thread(void *data) { .type = LSP_REQUEST_EXIT, .data = {{0}} }; + // just spam these things + // we're supposed to be nice and wait for the shutdown + // response, but who gives a fuck write_request(lsp, &shutdown); - // i give you ONE MILLISECOND to send your fucking shutdown response - time_sleep_ms(1); write_request(lsp, &exit); - // i give you ONE MILLISECOND to terminate - // I WILL KILL YOU IF IT TAKES ANY LONGER - time_sleep_ms(1); #if 0 char buf[1024]={0}; @@ -626,16 +664,22 @@ const char *lsp_document_path(LSP *lsp, LSPDocumentID document) { return path; } -LSP *lsp_create(const char *root_dir, const char *command, u16 port, const char *configuration, FILE *log) { +LSP *lsp_create(const LSPSetup *setup) { LSP *lsp = calloc(1, sizeof *lsp); if (!lsp) return NULL; if (!request_id_mutex) request_id_mutex = SDL_CreateMutex(); + const char *const command = setup->command; + const u16 port = setup->port; + const char *const root_dir = setup->root_dir; + const char *const configuration = setup->configuration; + static LSPID curr_id = 1; lsp->id = curr_id++; - lsp->log = log; + lsp->log = setup->log; lsp->port = port; + lsp->send_delay = setup->send_delay; #if DEBUG printf("Starting up LSP %p (ID %u) `%s` (port %u) in %s\n", @@ -811,6 +855,18 @@ LSPDocumentPosition lsp_location_end_position(LSPLocation location) { }; } +const uint32_t *lsp_completion_trigger_chars(LSP *lsp) { + return lsp->completion_trigger_chars; +} + +const uint32_t *lsp_signature_help_trigger_chars(LSP *lsp) { + return lsp->signature_help_trigger_chars; +} + +const uint32_t *lsp_signature_help_retrigger_chars(LSP *lsp) { + return lsp->signature_help_retrigger_chars; +} + bool lsp_covers_path(LSP *lsp, const char *path) { bool ret = false; SDL_LockMutex(lsp->workspace_folders_mutex); @@ -856,6 +912,26 @@ void lsp_cancel_request(LSP *lsp, LSPRequestID id) { } } +bool lsp_has_exited(LSP *lsp) { + return lsp->exited; +} + +bool lsp_is_initialized(LSP *lsp) { + return lsp->initialized; +} + +bool lsp_has_incremental_sync_support(LSP *lsp) { + return lsp->capabilities.incremental_sync_support; +} + +const char *lsp_get_command(LSP *lsp) { + return lsp->command; +} + +u16 lsp_get_port(LSP *lsp) { + return lsp->port; +} + void lsp_quit(void) { if (request_id_mutex) { SDL_DestroyMutex(request_id_mutex); @@ -616,21 +616,126 @@ typedef struct { bool range_formatting_support; } LSPCapabilities; -typedef struct LSP { +typedef struct LSP LSP; + +/// arguments to \ref lsp_create, but in `struct` form because +/// there are so many of them. +typedef struct { + /// root directory + const char *root_dir; + /// command to run to start server (set to `NULL` if LSP is assumed to already be running) + const char *command; + /// port which server is listening on (set to 0 for LSP over stdio) + u16 port; + /// configuration JSON + const char *configuration; + /// log file, or `NULL` to disable logging + FILE *log; + /// see `LSP::send_delay` + double send_delay; +} LSPSetup; + +/// Start up an LSP server. +LSP *lsp_create(const LSPSetup *setup); +/// get unique ID associated with this server. +u32 lsp_get_id(const LSP *lsp); +/// has the server been initialized? +bool lsp_is_initialized(LSP *lsp); +/// \returns the \ref LSPSetup::command value passed into \ref lsp_create +const char *lsp_get_command(LSP *lsp); +/// \returns the \ref LSPSetup::port value passed into \ref lsp_create +u16 lsp_get_port(LSP *lsp); +/// has the server exited? +bool lsp_has_exited(LSP *lsp); +/// Assiociate `id` with the LSP language identifier `lsp_identifier` (see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#-textdocumentitem-) +void lsp_register_language(u64 id, const char *lsp_identifier); +// returns true if there's an error. +// returns false and sets error to "" if there's no error. +// if clear = true, the error will be cleared. +// you can set error = NULL, error_size = 0, clear = true to just clear the error +bool lsp_get_error(LSP *lsp, char *error, size_t error_size, bool clear); +void lsp_message_free(LSPMessage *message); +u32 lsp_document_id(LSP *lsp, const char *path); +// returned pointer lives as long as lsp. +const char *lsp_document_path(LSP *lsp, LSPDocumentID id); +// returns the ID of the sent request, or (LSPServerRequestID){0} if the request is not supported by the LSP +// don't free the contents of this request (even on failure)! let me handle it! +LSPServerRequestID lsp_send_request(LSP *lsp, LSPRequest *request); +// send a $/cancelRequest notification +// if id = 0, nothing will happen. +void lsp_cancel_request(LSP *lsp, LSPRequestID id); +// don't free the contents of this response! let me handle it! +void lsp_send_response(LSP *lsp, LSPResponse *response); +const char *lsp_response_string(const LSPResponse *response, LSPString string); +const char *lsp_request_string(const LSPRequest *request, LSPString string); +/// low-level API for allocating message strings. +/// +/// sets `*string` to the LSPString, and returns a pointer which you can write the string to. +/// the returned pointer will be zeroed up to and including [len]. +char *lsp_message_alloc_string(LSPMessageBase *message, size_t len, LSPString *string); +LSPString lsp_message_add_string32(LSPMessageBase *message, String32 string); +LSPString lsp_request_add_string(LSPRequest *request, const char *string); +LSPString lsp_response_add_string(LSPResponse *response, const char *string); +bool lsp_string_is_empty(LSPString string); +// try to add a new "workspace folder" to the lsp. +// IMPORTANT: only call this if lsp->initialized is true +// (if not we don't yet know whether the server supports workspace folders) +// returns true on success or if new_root_dir is already contained in a workspace folder for this LSP. +// if this fails (i.e. if the LSP does not have workspace support), create a new LSP +// with root directory `new_root_dir`. +bool lsp_try_add_root_dir(LSP *lsp, const char *new_root_dir); +// is this path in the LSP's workspace folders? +bool lsp_covers_path(LSP *lsp, const char *path); +// get next message from server +bool lsp_next_message(LSP *lsp, LSPMessage *message); +/// returns `-1` if `a` comes before `b`, 0 if `a` and `b` are equal, and `1` if `a` comes after `b` +int lsp_position_cmp(LSPPosition a, LSPPosition b); +/// returns `true` if `a` and `b` are equal +bool lsp_position_eq(LSPPosition a, LSPPosition b); +/// returns `true` if `a` and `b` overlap +bool lsp_ranges_overlap(LSPRange a, LSPRange b); +bool lsp_document_position_eq(LSPDocumentPosition a, LSPDocumentPosition b); +/// does this server support incremental synchronization +/// +/// see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_synchronization +/// for more info. +bool lsp_has_incremental_sync_support(LSP *lsp); +/// get dynamic array of completion trigger characters. +const uint32_t *lsp_completion_trigger_chars(LSP *lsp); +/// get dynamic array of signature help trigger characters. +const uint32_t *lsp_signature_help_trigger_chars(LSP *lsp); +/// get dynamic array of signature help retrigger characters. +const uint32_t *lsp_signature_help_retrigger_chars(LSP *lsp); +// get the start of location's range as a LSPDocumentPosition +LSPDocumentPosition lsp_location_start_position(LSPLocation location); +// get the end of location's range as a LSPDocumentPosition +LSPDocumentPosition lsp_location_end_position(LSPLocation location); +void lsp_free(LSP *lsp); +// call this to free any global resources used by lsp*.c +// not strictly necessary, but prevents valgrind errors & stuff. +// make sure you call lsp_free on every LSP you create before calling this. +void lsp_quit(void); + +#endif // LSP_H_ + +#if defined LSP_INTERNAL && !defined LSP_INTERNAL_H_ +#define LSP_INTERNAL_H_ + +struct LSP { // thread safety is important here! // every member should either be indented to indicate which mutex controls it, // or have a comment explaining why it doesn't need one // A unique ID number for this LSP. - // thread-safety: only set once in lsp_create. + // thread-safety: only set once in \ref lsp_create. LSPID id; - // thread-safety: set once in lsp_create, then only used by communication thread + // thread-safety: set once in \ref lsp_create, then only used by communication thread FILE *log; // The server process. May be NULL if the process isn't started by ted. // - // thread-safety: created in lsp_create, then only accessed by the communication thread + // thread-safety: created in \ref lsp_create, then only accessed by the communication thread Process *process; // Socket for communicating with server. Maybe be NULL if communication is done over stdio. // @@ -640,9 +745,17 @@ typedef struct LSP { // port used for communication // // this will be zero iff communication is done over stdio - // thread-safety: only set once in lsp_create + // thread-safety: only set once in \ref lsp_create u16 port; + /// delay before sending requests. + /// + /// this exists for servers which don't support `$/cancelRequest` + /// to avoid flooding them with requests which they can't keep up with. + /// + /// thread-safety: only set once in \ref lsp_create + double send_delay; + LSPMutex document_mutex; // for our purposes, folders are "documents" // the spec kinda does this too: WorkspaceFolder has a `uri: DocumentUri` member. @@ -688,74 +801,7 @@ typedef struct LSP { LSPDocumentID *workspace_folders; LSPMutex error_mutex; char error[512]; -} LSP; - -/// Assiociate `id` with the LSP language identifier `lsp_identifier` (see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#-textdocumentitem-) -void lsp_register_language(u64 id, const char *lsp_identifier); -// returns true if there's an error. -// returns false and sets error to "" if there's no error. -// if clear = true, the error will be cleared. -// you can set error = NULL, error_size = 0, clear = true to just clear the error -bool lsp_get_error(LSP *lsp, char *error, size_t error_size, bool clear); -void lsp_message_free(LSPMessage *message); -u32 lsp_document_id(LSP *lsp, const char *path); -// returned pointer lives as long as lsp. -const char *lsp_document_path(LSP *lsp, LSPDocumentID id); -// returns the ID of the sent request, or (LSPServerRequestID){0} if the request is not supported by the LSP -// don't free the contents of this request (even on failure)! let me handle it! -LSPServerRequestID lsp_send_request(LSP *lsp, LSPRequest *request); -// send a $/cancelRequest notification -// if id = 0, nothing will happen. -void lsp_cancel_request(LSP *lsp, LSPRequestID id); -// don't free the contents of this response! let me handle it! -void lsp_send_response(LSP *lsp, LSPResponse *response); -const char *lsp_response_string(const LSPResponse *response, LSPString string); -const char *lsp_request_string(const LSPRequest *request, LSPString string); -/// low-level API for allocating message strings. -/// -/// sets `*string` to the LSPString, and returns a pointer which you can write the string to. -/// the returned pointer will be zeroed up to and including [len]. -char *lsp_message_alloc_string(LSPMessageBase *message, size_t len, LSPString *string); -LSPString lsp_message_add_string32(LSPMessageBase *message, String32 string); -LSPString lsp_request_add_string(LSPRequest *request, const char *string); -LSPString lsp_response_add_string(LSPResponse *response, const char *string); -bool lsp_string_is_empty(LSPString string); -/// Start up an LSP server. -/// -/// `command`, `port`, `configuration` and `log` can be 0 as long as `port` and `command` are not both 0. -LSP *lsp_create(const char *root_dir, const char *command, u16 port, const char *configuration, FILE *log); -// try to add a new "workspace folder" to the lsp. -// IMPORTANT: only call this if lsp->initialized is true -// (if not we don't yet know whether the server supports workspace folders) -// returns true on success or if new_root_dir is already contained in a workspace folder for this LSP. -// if this fails (i.e. if the LSP does not have workspace support), create a new LSP -// with root directory `new_root_dir`. -bool lsp_try_add_root_dir(LSP *lsp, const char *new_root_dir); -// is this path in the LSP's workspace folders? -bool lsp_covers_path(LSP *lsp, const char *path); -// get next message from server -bool lsp_next_message(LSP *lsp, LSPMessage *message); -/// returns `-1` if `a` comes before `b`, 0 if `a` and `b` are equal, and `1` if `a` comes after `b` -int lsp_position_cmp(LSPPosition a, LSPPosition b); -/// returns `true` if `a` and `b` are equal -bool lsp_position_eq(LSPPosition a, LSPPosition b); -/// returns `true` if `a` and `b` overlap -bool lsp_ranges_overlap(LSPRange a, LSPRange b); -bool lsp_document_position_eq(LSPDocumentPosition a, LSPDocumentPosition b); -// get the start of location's range as a LSPDocumentPosition -LSPDocumentPosition lsp_location_start_position(LSPLocation location); -// get the end of location's range as a LSPDocumentPosition -LSPDocumentPosition lsp_location_end_position(LSPLocation location); -void lsp_free(LSP *lsp); -// call this to free any global resources used by lsp*.c -// not strictly necessary, but prevents valgrind errors & stuff. -// make sure you call lsp_free on every LSP you create before calling this. -void lsp_quit(void); - -#endif // LSP_H_ - -#if defined LSP_INTERNAL && !defined LSP_INTERNAL_H_ -#define LSP_INTERNAL_H_ +}; #include "sdl-inc.h" @@ -871,9 +917,9 @@ LSPString lsp_request_add_json_string(LSPRequest *request, const JSON *json, JSO void lsp_write_quit(void); /// print server-to-client communication -#define LSP_SHOW_S2C 0 +#define LSP_SHOW_S2C 1 /// print client-to-server communication -#define LSP_SHOW_C2S 0 +#define LSP_SHOW_C2S 1 #endif // LSP_INTERNAL @@ -1,9 +1,8 @@ /* TODO: -- figure out how to deal with godot language server being so slow - one comparatively solution is to wait x seconds before sending a batch of requests in the communication thread - (this gives us time to cancel the irrelevant requests before they get sent to the server, - and we can remove stale full-sync didChange requests) +- on the last line of a buffer, shift+up end backspace acts weird +- restart LSP server automatically? + FUTURE FEATURES: - autodetect indentation (tabs vs spaces) - custom file/build command associations @@ -764,7 +763,7 @@ int main(int argc, char **argv) { char32_t last_char = 0; unicode_utf8_to_utf32(&last_char, &text[last_code_point], strlen(text) - last_code_point); - arr_foreach_ptr(lsp->completion_trigger_chars, char32_t, c) { + arr_foreach_ptr(lsp_completion_trigger_chars(lsp), const char32_t, c) { if (*c == last_char) { autocomplete_open(ted, last_char); break; diff --git a/ted-internal.h b/ted-internal.h index 498516b..0efa6b6 100644 --- a/ted-internal.h +++ b/ted-internal.h @@ -104,6 +104,7 @@ struct Settings { float cursor_blink_time_on, cursor_blink_time_off; float hover_time; float ctrl_scroll_adjust_text_size; + float lsp_delay; u32 max_file_size; u32 max_file_size_view_only; u16 framerate_cap; @@ -230,8 +230,8 @@ LSP *ted_get_lsp_by_id(Ted *ted, LSPID id) { if (id == 0) return NULL; for (int i = 0; ted->lsps[i]; ++i) { LSP *lsp = ted->lsps[i]; - if (lsp->id == id) - return lsp->exited ? NULL : lsp; + if (lsp_get_id(lsp) == id) + return lsp_has_exited(lsp) ? NULL : lsp; } return NULL; } @@ -245,16 +245,18 @@ LSP *ted_get_lsp(Ted *ted, const char *path, Language language) { for (i = 0; i < TED_LSP_MAX; ++i) { LSP *lsp = ted->lsps[i]; if (!lsp) break; - if (lsp->command && !streq(lsp->command, settings->lsp)) continue; - if (lsp->port != settings->lsp_port) continue; + const char *const lsp_command = lsp_get_command(lsp); + const u16 lsp_port = lsp_get_port(lsp); + if (lsp_command && !streq(lsp_command, settings->lsp)) continue; + if (lsp_port != settings->lsp_port) continue; - if (!lsp->initialized) { + if (!lsp_is_initialized(lsp)) { // withhold judgement until this server initializes. // we shouldn't call lsp_try_add_root_dir yet because it doesn't know // if the server supports workspaceFolders. return NULL; } - if (lsp_covers_path(lsp, path) && lsp->exited) { + if (lsp_covers_path(lsp, path) && lsp_has_exited(lsp)) { // this server died. give up. return NULL; } @@ -271,8 +273,15 @@ LSP *ted_get_lsp(Ted *ted, const char *path, Language language) { // start up this LSP FILE *log = settings->lsp_log ? ted->log : NULL; char *root_dir = settings_get_root_dir(settings, path); - ted->lsps[i] = lsp_create(root_dir, *settings->lsp ? settings->lsp : NULL, - settings->lsp_port, settings->lsp_configuration, log); + LSPSetup setup = { + .root_dir = root_dir, + .command = *settings->lsp ? settings->lsp : NULL, + .port = settings->lsp_port, + .configuration = settings->lsp_configuration, + .log = log, + .send_delay = settings->lsp_delay, + }; + ted->lsps[i] = lsp_create(&setup); free(root_dir); // don't actually return it yet, since it's still initializing (see above) } @@ -211,6 +211,15 @@ comment-end = " */" [GDScript.core] lsp-port = 6005 +# this delay is needed because godot's language server is currently kinda shitty +# and slow so we want to avoid overwhelming it with requests +# (specifically this sets up a delay between you typing +# and ted sending what you typed to godot) +lsp-delay = 0.5 +# phantom completion/signature help use a lot of requests --- let's not overwhelm godot +# (turn these back on if you want but you may have to increase lsp-delay) +phantom-completions = off +signature-help = off # phantom completions are just annoying if you're not actually programming [Markdown.core] @@ -134,6 +134,9 @@ typedef struct Selector Selector; /// a selector menu for files (e.g. the "open" menu) typedef struct FileSelector FileSelector; +/// LSP server +struct LSP; + /// an entry in a \ref Selector /// /// only `name` needs to be filled in; everything else can be zeroed. |