diff options
-rw-r--r-- | buffer.c | 4 | ||||
-rw-r--r-- | ide-autocomplete.c (renamed from autocomplete.c) | 0 | ||||
-rw-r--r-- | ide-definitions.c | 41 | ||||
-rw-r--r-- | ide-hover.c (renamed from hover.c) | 0 | ||||
-rw-r--r-- | ide-signature-help.c (renamed from signature-help.c) | 0 | ||||
-rw-r--r-- | lsp-json.c (renamed from json.c) | 4 | ||||
-rw-r--r-- | lsp-parse.c | 73 | ||||
-rw-r--r-- | lsp-write.c | 13 | ||||
-rw-r--r-- | lsp.c | 25 | ||||
-rw-r--r-- | lsp.h | 22 | ||||
-rw-r--r-- | main.c | 13 | ||||
-rw-r--r-- | ted.c | 33 | ||||
-rw-r--r-- | ted.h | 19 |
13 files changed, 234 insertions, 13 deletions
@@ -2489,12 +2489,14 @@ bool buffer_handle_click(Ted *ted, TextBuffer *buffer, v2 click, u8 times) { break; case KEY_MODIFIER_CTRL: if (!buffer->is_line_buffer) { + // go to definition buffer_cursor_move_to_pos(buffer, buffer_pos); String32 word = buffer_word_at_cursor(buffer); if (word.len) { char *tag = str32_to_utf8_cstr(word); if (tag) { - tag_goto(buffer->ted, tag); + LSPDocumentPosition pos = buffer_pos_to_lsp_document_position(buffer, buffer_pos); + definition_goto(buffer->ted, buffer_lsp(buffer), tag, pos); free(tag); } } diff --git a/autocomplete.c b/ide-autocomplete.c index f1c3c13..f1c3c13 100644 --- a/autocomplete.c +++ b/ide-autocomplete.c diff --git a/ide-definitions.c b/ide-definitions.c new file mode 100644 index 0000000..3a36a96 --- /dev/null +++ b/ide-definitions.c @@ -0,0 +1,41 @@ +void definition_goto(Ted *ted, LSP *lsp, const char *name, LSPDocumentPosition position) { + if (lsp) { + // send that request + LSPRequest request = {.type = LSP_REQUEST_DEFINITION}; + request.data.definition.position = position; + lsp_send_request(lsp, &request); + } else { + // just go to the tag + tag_goto(ted, name); + } +} + +void definitions_process_lsp_response(Ted *ted, LSP *lsp, const LSPResponse *response) { + if (response->request.type != LSP_REQUEST_DEFINITION) + return; + + const LSPResponseDefinition *response_def = &response->data.definition; + Definitions *defs = &ted->definitions; + + if (defs->last_response_lsp == lsp->id + && response->request.id < defs->last_response_id) { + // we just processed a later response, so let's ignore this + return; + } + defs->last_response_lsp = lsp->id; + defs->last_response_id = response->request.id; + + if (!arr_len(response_def->locations)) { + // no definition. do the error cursor. + ted_flash_error_cursor(ted); + return; + } + LSPLocation location = response_def->locations[0]; + const char *path = lsp_document_path(lsp, location.document); + if (!ted_open_file(ted, path)) { + ted_flash_error_cursor(ted); + return; + } + LSPDocumentPosition position = lsp_location_start_position(location); + ted_go_to_lsp_document_position(ted, lsp, position); +} diff --git a/signature-help.c b/ide-signature-help.c index 97dc8c3..97dc8c3 100644 --- a/signature-help.c +++ b/ide-signature-help.c @@ -545,6 +545,10 @@ JSONArray json_array_get_array(const JSON *json, JSONArray array, size_t i) { return json_force_array(json_array_get(json, array, i)); } +JSONValue json_root(const JSON *json) { + return json->values[0]; +} + // e.g. if json is { "a" : { "b": 3 }}, then json_get(json, "a.b") = 3. // returns undefined if there is no such property JSONValue json_get(const JSON *json, const char *path) { diff --git a/lsp-parse.c b/lsp-parse.c index 07837b5..a892539 100644 --- a/lsp-parse.c +++ b/lsp-parse.c @@ -25,6 +25,7 @@ static WarnUnusedResult bool lsp_expect_number(LSP *lsp, JSONValue value, const return lsp_expect_type(lsp, value, JSON_NUMBER, what); } + static LSPString lsp_response_add_json_string(LSPResponse *response, const JSON *json, JSONString string) { u32 offset = arr_len(response->string_data); arr_set_len(response->string_data, offset + string.len + 1); @@ -80,6 +81,24 @@ static bool parse_range(LSP *lsp, const JSON *json, JSONValue range_value, LSPRa return success; } +static bool parse_document_uri(LSP *lsp, const JSON *json, JSONValue value, LSPDocumentID *id) { + if (value.type != JSON_STRING) { + lsp_set_error(lsp, "Expected string for URI, got %s", + json_type_to_str(value.type)); + return false; + } + char *string = json_string_get_alloc(json, value.val.string); + if (!str_has_prefix(string, "file://")) { + lsp_set_error(lsp, "Can't process non-local URI %s", + string); + free(string); + return false; + } + *id = lsp_document_id(lsp, string + strlen("file://")); + free(string); + return true; +} + static uint32_t *parse_trigger_characters(const JSON *json, JSONArray trigger_chars) { uint32_t *array = NULL; @@ -136,6 +155,12 @@ static void parse_capabilities(LSP *lsp, const JSON *json, JSONObject capabiliti cap->hover_support = true; } + // check for definition support + JSONValue definition_value = json_object_get(json, capabilities, "definitionProvider"); + if (definition_value.type != JSON_UNDEFINED) { + cap->definition_support = true; + } + JSONObject workspace = json_object_get_object(json, capabilities, "workspace"); // check WorkspaceFoldersServerCapabilities JSONObject workspace_folders = json_object_get_object(json, workspace, "workspaceFolders"); @@ -208,7 +233,7 @@ static bool parse_completion(LSP *lsp, const JSON *json, LSPResponse *response) item->text_edit = (LSPTextEdit) { .type = LSP_TEXT_EDIT_PLAIN, .at_cursor = true, - .range = {{0}}, + .range = {{0}, {0}}, .new_text = item->label }; @@ -462,6 +487,47 @@ static bool parse_hover(LSP *lsp, const JSON *json, LSPResponse *response) { return true; } +// parse a Location: {uri: DocumentUri, range: Range} +static bool parse_location(LSP *lsp, const JSON *json, JSONValue value, LSPLocation *location) { + if (value.type != JSON_OBJECT) { + lsp_set_error(lsp, "Expected object for location but got %s", + json_type_to_str(value.type)); + return false; + } + JSONObject object = value.val.object; + JSONValue uri = json_object_get(json, object, "uri"); + if (!parse_document_uri(lsp, json, uri, &location->document)) + return false; + JSONValue range = json_object_get(json, object, "range"); + if (!parse_range(lsp, json, range, &location->range)) + return false; + return true; +} + +static bool parse_definition(LSP *lsp, const JSON *json, LSPResponse *response) { + JSONValue result = json_get(json, "result"); + if (result.type == JSON_NULL) { + // no location + return true; + } + LSPResponseDefinition *definition = &response->data.definition; + if (result.type == JSON_ARRAY) { + JSONArray locations = result.val.array; + if (locations.len == 0) + return true; + for (u32 l = 0; l < locations.len; ++l) { + JSONValue location_json = json_array_get(json, locations, l); + LSPLocation *location = arr_addp(definition->locations); + if (!parse_location(lsp, json, location_json, location)) + return false; + } + return true; + } else { + LSPLocation *location = arr_addp(definition->locations); + return parse_location(lsp, json, result, location); + } +} + // fills request->id/id_string appropriately given the request's json // returns true on success static WarnUnusedResult bool parse_id(JSON *json, LSPRequest *request) { @@ -552,6 +618,8 @@ static bool parse_server2client_request(LSP *lsp, JSON *json, LSPRequest *reques return false; } + + static void process_message(LSP *lsp, JSON *json) { #if 0 @@ -601,6 +669,9 @@ static void process_message(LSP *lsp, JSON *json) { case LSP_REQUEST_HOVER: add_to_messages = parse_hover(lsp, json, &response); break; + case LSP_REQUEST_DEFINITION: + add_to_messages = parse_definition(lsp, json, &response); + break; case LSP_REQUEST_INITIALIZE: { // it's the response to our initialize request! if (result.type == JSON_OBJECT) { diff --git a/lsp-write.c b/lsp-write.c index 7db0f66..7d8a0f1 100644 --- a/lsp-write.c +++ b/lsp-write.c @@ -1,3 +1,5 @@ +#define write_bool lsp_write_bool // prevent naming conflict + static const char *lsp_language_id(Language lang) { switch (lang) { case LANG_CONFIG: @@ -267,6 +269,8 @@ static const char *lsp_request_method(LSPRequest *request) { return "textDocument/signatureHelp"; case LSP_REQUEST_HOVER: return "textDocument/hover"; + case LSP_REQUEST_DEFINITION: + return "textDocument/definition"; case LSP_REQUEST_WORKSPACE_FOLDERS: return "workspace/workspaceFolders"; case LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS: @@ -296,6 +300,7 @@ static bool request_type_is_notification(LSPRequestType type) { case LSP_REQUEST_COMPLETION: case LSP_REQUEST_SIGNATURE_HELP: case LSP_REQUEST_HOVER: + case LSP_REQUEST_DEFINITION: case LSP_REQUEST_WORKSPACE_FOLDERS: return false; } @@ -512,6 +517,12 @@ static void write_request(LSP *lsp, LSPRequest *request) { write_document_position(o, hover->position); write_obj_end(o); } break; + case LSP_REQUEST_DEFINITION: { + const LSPRequestDefinition *def = &request->data.definition; + write_key_obj_start(o, "params"); + write_document_position(o, def->position); + write_obj_end(o); + } break; case LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS: { const LSPRequestDidChangeWorkspaceFolders *w = &request->data.change_workspace_folders; write_key_obj_start(o, "params"); @@ -605,3 +616,5 @@ static void write_message(LSP *lsp, LSPMessage *message) { // (as i'm writing this, this won't do anything but it might in the future) lsp_message_free(message); } + +#undef write_bool @@ -3,8 +3,6 @@ // print client-to-server communication #define LSP_SHOW_C2S 0 -#define write_bool lsp_write_bool - static void lsp_request_free(LSPRequest *r); static void lsp_response_free(LSPResponse *r); @@ -13,6 +11,7 @@ static void lsp_response_free(LSPResponse *r); strbuf_printf(lsp->error, __VA_ARGS__);\ SDL_UnlockMutex(lsp->error_mutex);\ } while (0) +#include "lsp-json.c" #include "lsp-write.c" #include "lsp-parse.c" @@ -44,6 +43,7 @@ static void lsp_request_free(LSPRequest *r) { case LSP_REQUEST_COMPLETION: case LSP_REQUEST_SIGNATURE_HELP: case LSP_REQUEST_HOVER: + case LSP_REQUEST_DEFINITION: case LSP_REQUEST_DID_CLOSE: case LSP_REQUEST_WORKSPACE_FOLDERS: case LSP_REQUEST_JDTLS_CONFIGURATION: @@ -80,6 +80,9 @@ static void lsp_response_free(LSPResponse *r) { case LSP_REQUEST_SIGNATURE_HELP: arr_free(r->data.signature_help.signatures); break; + case LSP_REQUEST_DEFINITION: + arr_free(r->data.definition.locations); + break; default: break; } @@ -145,6 +148,8 @@ static bool lsp_supports_request(LSP *lsp, const LSPRequest *request) { return cap->workspace_folders_support; case LSP_REQUEST_HOVER: return cap->hover_support; + case LSP_REQUEST_DEFINITION: + return cap->definition_support; } assert(0); return false; @@ -542,4 +547,18 @@ bool lsp_document_position_eq(LSPDocumentPosition a, LSPDocumentPosition b) { return a.document == b.document && lsp_position_eq(a.pos, b.pos); } -#undef write_bool + +LSPDocumentPosition lsp_location_start_position(LSPLocation location) { + return (LSPDocumentPosition) { + .document = location.document, + .pos = location.range.start + }; +} + +LSPDocumentPosition lsp_location_end_position(LSPLocation location) { + return (LSPDocumentPosition) { + .document = location.document, + .pos = location.range.end + }; +} + @@ -26,6 +26,11 @@ typedef struct { LSPPosition end; } LSPRange; +typedef struct { + LSPDocumentID document; + LSPRange range; +} LSPLocation; + typedef enum { LSP_REQUEST_NONE, @@ -44,6 +49,7 @@ typedef enum { LSP_REQUEST_COMPLETION, // textDocument/completion LSP_REQUEST_SIGNATURE_HELP, // textDocument/signatureHelp LSP_REQUEST_HOVER, // textDocument/hover + LSP_REQUEST_DEFINITION, // textDocument/definition LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS, // workspace/didChangeWorkspaceFolders // server-to-client @@ -117,6 +123,10 @@ typedef struct { } LSPRequestHover; typedef struct { + LSPDocumentPosition position; +} LSPRequestDefinition; + +typedef struct { LSPDocumentID *removed; // dynamic array LSPDocumentID *added; // dynamic array } LSPRequestDidChangeWorkspaceFolders; @@ -134,6 +144,7 @@ typedef struct { LSPRequestCompletion completion; LSPRequestSignatureHelp signature_help; LSPRequestHover hover; + LSPRequestDefinition definition; // LSP_REQUEST_SHOW_MESSAGE or LSP_REQUEST_LOG_MESSAGE LSPRequestMessage message; LSPRequestDidChangeWorkspaceFolders change_workspace_folders; @@ -279,6 +290,10 @@ typedef struct { LSPString contents; } LSPResponseHover; +typedef struct { + // where the symbol is defined (dynamic array) + LSPLocation *locations; +} LSPResponseDefinition; typedef LSPRequestType LSPResponseType; typedef struct { @@ -292,6 +307,7 @@ typedef struct { LSPResponseCompletion completion; LSPResponseSignatureHelp signature_help; LSPResponseHover hover; + LSPResponseDefinition definition; } data; } LSPResponse; @@ -312,6 +328,7 @@ typedef struct { bool signature_help_support; bool completion_support; bool hover_support; + bool definition_support; // support for multiple root folders // sadly, as of me writing this, clangd and rust-analyzer don't support this // (but jdtls and gopls do) @@ -398,8 +415,11 @@ LSP *lsp_create(const char *root_dir, Language language, const char *analyzer_co // 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); +// report that this document has changed +void lsp_document_changed(LSP *lsp, const char *document, LSPDocumentChangeEvent change); bool lsp_next_message(LSP *lsp, LSPMessage *message); bool lsp_position_eq(LSPPosition a, LSPPosition b); bool lsp_document_position_eq(LSPDocumentPosition a, LSPDocumentPosition b); -void lsp_document_changed(LSP *lsp, const char *document, LSPDocumentChangeEvent change); +LSPDocumentPosition lsp_location_start_position(LSPLocation location); +LSPDocumentPosition lsp_location_end_position(LSPLocation location); void lsp_free(LSP *lsp); @@ -1,5 +1,7 @@ /* @TODO: +- check definition capabilities +- some way of showing that we're currently loading the definition location (different cursor color?) - more LSP stuff: - go to definition using LSP - find usages @@ -13,9 +15,11 @@ - what to do if initialize request takes a long time? - delete old sent requests? but make sure requests that just take a long time are okay. (if the server never sends a response) +- check that tags still works - TESTING: make rust-analyzer-slow (waits 10s before sending response) - run everything through valgrind ideally with leak checking - grep -i -n TODO *.[ch] +- when searching files/definitions, sort by length? or put exact matches at the top? --- LSP MERGE --- - improve structure of ted source code to make LSP completions better (make every c file a valid translation unit) @@ -140,13 +144,13 @@ bool tag_goto(Ted *ted, char const *tag); #include "build.c" #include "tags.c" #include "menu.c" -#include "autocomplete.c" -#include "signature-help.c" -#include "hover.c" +#include "ide-autocomplete.c" +#include "ide-signature-help.c" +#include "ide-hover.c" +#include "ide-definitions.c" #include "command.c" #include "config.c" #include "session.c" -#include "json.c" #include "lsp.c" #if PROFILE @@ -891,6 +895,7 @@ int main(int argc, char **argv) { autocomplete_process_lsp_response(ted, r); signature_help_process_lsp_response(ted, r); hover_process_lsp_response(ted, r); + definitions_process_lsp_response(ted, lsp, r); } break; } lsp_message_free(&message); @@ -507,3 +507,36 @@ void ted_press_key(Ted *ted, SDL_Scancode scancode, SDL_Keymod modifier) { } } } + +// make the cursor red for a bit to indicate an error (e.g. no autocompletions) +void ted_flash_error_cursor(Ted *ted) { + ted->cursor_error_time = time_get_seconds(); +} + +void ted_go_to_position(Ted *ted, const char *path, u32 line, u32 index, bool is_lsp) { + if (ted_open_file(ted, path)) { + TextBuffer *buffer = ted->active_buffer; + BufferPos pos = {0}; + if (is_lsp) { + LSPPosition lsp_pos = { + .line = line, + .character = index + }; + pos = buffer_pos_from_lsp(buffer, lsp_pos); + } else { + pos.line = line; + pos.index = index; + } + buffer_cursor_move_to_pos(buffer, pos); + buffer->center_cursor_next_frame = true; + } else { + ted_flash_error_cursor(ted); + } +} + +void ted_go_to_lsp_document_position(Ted *ted, LSP *lsp, LSPDocumentPosition position) { + const char *path = lsp_document_path(lsp, position.document); + u32 line = position.pos.line; + u32 character = position.pos.character; + ted_go_to_position(ted, path, line, character, true); +} @@ -413,6 +413,14 @@ typedef struct { BufferPos range_end; } Hover; +typedef struct { + + // LSPID and ID of the last response which was processed. + // used to process responses in chronological order (= ID order) + LSPID last_response_lsp; + u32 last_response_id; +} Definitions; + typedef struct Ted { struct LSP *lsps[TED_LSP_MAX + 1]; @@ -470,6 +478,7 @@ typedef struct Ted { Autocomplete autocomplete; SignatureHelp signature_help; Hover hover; + Definitions definitions; FILE *log; @@ -530,9 +539,6 @@ typedef struct Ted { char error[512]; char error_shown[512]; // error display in box on screen } Ted; - -void autocomplete_close(Ted *ted); -void signature_help_retrigger(Ted *ted); char *buffer_contents_utf8_alloc(TextBuffer *buffer); Command command_from_str(char const *str); void command_execute(Ted *ted, Command c, i64 argument); @@ -566,3 +572,10 @@ char *settings_get_root_dir(Settings *settings, const char *path); void menu_open(Ted *ted, Menu menu); void menu_close(Ted *ted); void find_update(Ted *ted, bool force); +void autocomplete_close(Ted *ted); +void signature_help_retrigger(Ted *ted); +// go to the definition of `name`. +// if `lsp` is NULL, tags will be used. +// Note: the document position is required for LSP requests because of overloading (where the name +// alone isn't sufficient) +void definition_goto(Ted *ted, LSP *lsp, const char *name, LSPDocumentPosition pos); |