diff options
-rw-r--r-- | README.md | 5 | ||||
-rw-r--r-- | json.c | 2 | ||||
-rw-r--r-- | lsp-parse.c | 16 | ||||
-rw-r--r-- | lsp-write.c | 78 | ||||
-rw-r--r-- | lsp.c | 60 | ||||
-rw-r--r-- | lsp.h | 41 | ||||
-rw-r--r-- | main.c | 7 | ||||
-rw-r--r-- | ted.c | 28 | ||||
-rw-r--r-- | ted.cfg | 2 | ||||
-rw-r--r-- | test/lsp/JavaA/.ted-root | 0 | ||||
-rw-r--r-- | test/lsp/JavaA/Main.java | 8 | ||||
-rw-r--r-- | test/lsp/JavaB/.ted-root | 0 | ||||
-rw-r--r-- | test/lsp/JavaB/B.java | 13 | ||||
-rw-r--r-- | test/test.cpp (renamed from test.cpp) | 0 | ||||
-rw-r--r-- | test/test.go (renamed from test.go) | 0 | ||||
-rw-r--r-- | test/test.java (renamed from test.java) | 0 | ||||
-rw-r--r-- | test/test.js (renamed from test.js) | 0 | ||||
-rw-r--r-- | test/test.rs (renamed from test.rs) | 0 |
18 files changed, 211 insertions, 49 deletions
@@ -234,3 +234,8 @@ ted is in the public domain (see `LICENSE.txt`). You can report a bug by sending an email to `pommicket at pommicket.com`. +If ted is crashing on startup try doing these things: + +- Delete `~/.local/share/ted/session.txt` or `C:\Users\<your user name>\AppData\Local\ted\session.txt` +- Reset your ted configuration by moving `ted.cfg` somewhere else. + @@ -2,7 +2,6 @@ // provides FAST(ish) parsing but SLOW lookup // this is especially fast for small objects // this actually supports "extended json", where objects can have arbitrary values as keys. - typedef struct { u32 pos; u32 len; @@ -57,6 +56,7 @@ typedef struct { JSONValue *values; } JSON; + #define SKIP_WHITESPACE while (json_is_space(text[index])) ++index; const char *json_type_to_str(JSONValueType type) { diff --git a/lsp-parse.c b/lsp-parse.c index 6d436d9..8797edf 100644 --- a/lsp-parse.c +++ b/lsp-parse.c @@ -79,9 +79,12 @@ static bool parse_range(LSP *lsp, const JSON *json, JSONValue range_value, LSPRa static void parse_capabilities(LSP *lsp, const JSON *json, JSONObject capabilities) { + LSPCapabilities *cap = &lsp->capabilities; + + // check CompletionOptions JSONValue completion_value = json_object_get(json, capabilities, "completionProvider"); if (completion_value.type == JSON_OBJECT) { - lsp->provides_completion = true; + cap->completion_support = true; JSONObject completion = completion_value.val.object; JSONArray trigger_chars = json_object_get_array(json, completion, "triggerCharacters"); @@ -104,6 +107,14 @@ static void parse_capabilities(LSP *lsp, const JSON *json, JSONObject capabiliti } } } + + + JSONObject workspace = json_object_get_object(json, capabilities, "workspace"); + // check WorkspaceFoldersServerCapabilities + JSONObject workspace_folders = json_object_get_object(json, workspace, "workspaceFolders"); + if (json_object_get_bool(json, workspace_folders, "supported", false)) { + cap->workspace_folders_support = true; + } } static bool parse_completion(LSP *lsp, const JSON *json, LSPResponse *response) { @@ -383,15 +394,12 @@ static void process_message(LSP *lsp, JSON *json) { break; case LSP_REQUEST_INITIALIZE: { // it's the response to our initialize request! - if (result.type == JSON_OBJECT) { // read server capabilities JSONObject capabilities = json_object_get_object(json, result.val.object, "capabilities"); parse_capabilities(lsp, json, capabilities); } - // let's send back an "initialized" request (notification) because apparently - // that's something we need to do. LSPRequest initialized = { .type = LSP_REQUEST_INITIALIZED, .data = {0}, diff --git a/lsp-write.c b/lsp-write.c index 5d402c4..93bdd26 100644 --- a/lsp-write.c +++ b/lsp-write.c @@ -32,6 +32,13 @@ static const char *lsp_language_id(Language lang) { return "text"; } +static const char *lsp_document_path(LSP *lsp, LSPDocumentID document) { + if (document >= arr_len(lsp->document_data)) { + assert(0); + return ""; + } + return lsp->document_data[document].path; +} typedef struct { LSP *lsp; @@ -174,13 +181,7 @@ static void write_file_uri_direct(JSONWriter *o, const char *path) { } static void write_file_uri(JSONWriter *o, LSPDocumentID document) { - if (document >= arr_len(o->lsp->document_data)) { - assert(0); - str_builder_append(&o->builder, "\"\""); - return; - } - const char *path = o->lsp->document_data[document].path; - write_file_uri_direct(o, path); + write_file_uri_direct(o, lsp_document_path(o->lsp, document)); } static void write_key_file_uri(JSONWriter *o, const char *key, LSPDocumentID document) { @@ -217,13 +218,18 @@ static void write_key_range(JSONWriter *o, const char *key, LSPRange range) { write_range(o, range); } -static void write_workspace_folders(JSONWriter *o, char **workspace_folders) { +static void write_workspace_folder(JSONWriter *o, LSPDocumentID folder) { + write_obj_start(o); + write_key_file_uri(o, "uri", folder); + write_key_string(o, "name", lsp_document_path(o->lsp, folder)); + write_obj_end(o); +} + +static void write_workspace_folders(JSONWriter *o, LSPDocumentID *workspace_folders) { write_arr_start(o); - arr_foreach_ptr(workspace_folders, char *, folder) { - write_arr_elem_obj_start(o); - write_key_file_uri_direct(o, "uri", *folder); - write_key_string(o, "name", *folder); - write_obj_end(o); + arr_foreach_ptr(workspace_folders, LSPDocumentID, folder) { + write_arr_elem(o); + write_workspace_folder(o, *folder); } write_arr_end(o); } @@ -231,14 +237,18 @@ static void write_workspace_folders(JSONWriter *o, char **workspace_folders) { static const char *lsp_request_method(LSPRequest *request) { switch (request->type) { case LSP_REQUEST_NONE: break; - case LSP_REQUEST_SHOW_MESSAGE: - return "window/showMessage"; - case LSP_REQUEST_LOG_MESSAGE: - return "window/logMessage"; case LSP_REQUEST_INITIALIZE: return "initialize"; case LSP_REQUEST_INITIALIZED: return "initialized"; + case LSP_REQUEST_SHUTDOWN: + return "shutdown"; + case LSP_REQUEST_EXIT: + return "exit"; + case LSP_REQUEST_SHOW_MESSAGE: + return "window/showMessage"; + case LSP_REQUEST_LOG_MESSAGE: + return "window/logMessage"; case LSP_REQUEST_DID_OPEN: return "textDocument/didOpen"; case LSP_REQUEST_DID_CLOSE: @@ -247,12 +257,10 @@ static const char *lsp_request_method(LSPRequest *request) { return "textDocument/didChange"; case LSP_REQUEST_COMPLETION: return "textDocument/completion"; - case LSP_REQUEST_SHUTDOWN: - return "shutdown"; - case LSP_REQUEST_EXIT: - return "exit"; case LSP_REQUEST_WORKSPACE_FOLDERS: return "workspace/workspaceFolders"; + case LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS: + return "workspace/didChangeWorkspaceFolders"; } assert(0); return "$/ignore"; @@ -266,6 +274,7 @@ static bool request_type_is_notification(LSPRequestType type) { case LSP_REQUEST_DID_OPEN: case LSP_REQUEST_DID_CLOSE: case LSP_REQUEST_DID_CHANGE: + case LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS: return true; case LSP_REQUEST_INITIALIZE: case LSP_REQUEST_SHUTDOWN: @@ -385,9 +394,11 @@ static void write_request(LSP *lsp, LSPRequest *request) { write_key_bool(o, "workspaceFolders", true); write_obj_end(o); write_obj_end(o); - write_key_file_uri_direct(o, "rootUri", lsp->workspace_folders[0]); + SDL_LockMutex(lsp->workspace_folders_mutex); + write_key_file_uri(o, "rootUri", lsp->workspace_folders[0]); write_key(o, "workspaceFolders"); write_workspace_folders(o, lsp->workspace_folders); + SDL_UnlockMutex(lsp->workspace_folders_mutex); write_key_obj_start(o, "clientInfo"); write_key_string(o, "name", "ted"); write_obj_end(o); @@ -454,6 +465,25 @@ static void write_request(LSP *lsp, LSPRequest *request) { } 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"); + write_key_obj_start(o, "event"); + write_key_arr_start(o, "added"); + arr_foreach_ptr(w->added, LSPDocumentID, added) { + write_arr_elem(o); + write_workspace_folder(o, *added); + } + write_arr_end(o); + write_key_arr_start(o, "removed"); + arr_foreach_ptr(w->removed, LSPDocumentID, removed) { + write_arr_elem(o); + write_workspace_folder(o, *removed); + } + write_arr_end(o); + write_obj_end(o); + write_obj_end(o); + } break; } write_obj_end(o); @@ -486,7 +516,9 @@ static void write_response(LSP *lsp, LSPResponse *response) { write_key(o, "result"); switch (response->request.type) { case LSP_REQUEST_WORKSPACE_FOLDERS: - write_workspace_folders(o, lsp->workspace_folders); + SDL_LockMutex(lsp->workspace_folders_mutex); + write_workspace_folders(o, lsp->workspace_folders); + SDL_UnlockMutex(lsp->workspace_folders_mutex); break; default: // this is not a valid client-to-server response. @@ -1,7 +1,7 @@ // print server-to-client communication #define LSP_SHOW_S2C 0 // print client-to-server communication -#define LSP_SHOW_C2S 0 +#define LSP_SHOW_C2S 1 #define write_bool lsp_write_bool @@ -60,6 +60,11 @@ static void lsp_request_free(LSPRequest *r) { lsp_document_change_event_free(event); arr_free(c->changes); } break; + case LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS: { + LSPRequestDidChangeWorkspaceFolders *w = &r->data.change_workspace_folders; + arr_free(w->added); + arr_free(w->removed); + } break; } memset(r, 0, sizeof *r); } @@ -108,6 +113,7 @@ static bool has_response(const char *data, size_t data_len, u64 *p_offset, u64 * } static bool lsp_supports_request(LSP *lsp, const LSPRequest *request) { + LSPCapabilities *cap = &lsp->capabilities; switch (request->type) { case LSP_REQUEST_NONE: // return false for server-to-client requests since we should never send them @@ -124,7 +130,9 @@ static bool lsp_supports_request(LSP *lsp, const LSPRequest *request) { case LSP_REQUEST_EXIT: return true; case LSP_REQUEST_COMPLETION: - return lsp->provides_completion; + return cap->completion_support; + case LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS: + return cap->workspace_folders_support; } assert(0); return false; @@ -323,10 +331,12 @@ LSP *lsp_create(const char *root_dir, Language language, const char *analyzer_co #endif str_hash_table_create(&lsp->document_ids, sizeof(u32)); - arr_add(lsp->workspace_folders, str_dup(root_dir)); lsp->language = language; lsp->quit_sem = SDL_CreateSemaphore(0); + lsp->error_mutex = SDL_CreateMutex(); lsp->messages_mutex = SDL_CreateMutex(); + arr_add(lsp->workspace_folders, lsp_document_id(lsp, root_dir)); + lsp->workspace_folders_mutex = SDL_CreateMutex(); ProcessSettings settings = { .stdin_blocking = true, @@ -346,6 +356,46 @@ LSP *lsp_create(const char *root_dir, Language language, const char *analyzer_co return lsp; } +bool lsp_try_add_root_dir(LSP *lsp, const char *new_root_dir) { + bool got_it = false; + SDL_LockMutex(lsp->workspace_folders_mutex); + if (!lsp->initialized) { + // pretend we have workspace folder support until we get initialize response + // (it's totally possible that this would be called with lsp->initialized = false, + // e.g. if the user starts up ted with multiple files in different projects) + // we'll fix things up when we get the initialize response if there's no actual support. + arr_add(lsp->workspace_folders, lsp_document_id(lsp, new_root_dir)); + got_it = true; + } else { + arr_foreach_ptr(lsp->workspace_folders, LSPDocumentID, folder) { + if (str_has_path_prefix(new_root_dir, lsp_document_path(lsp, *folder))) { + got_it = true; + break; + } + } + } + SDL_UnlockMutex(lsp->workspace_folders_mutex); + if (got_it) return true; + + if (!lsp->capabilities.workspace_folders_support) { + return false; + } + + // send workspace/didChangeWorkspaceFolders notification + LSPRequest req = {.type = LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS}; + LSPRequestDidChangeWorkspaceFolders *w = &req.data.change_workspace_folders; + LSPDocumentID document_id = lsp_document_id(lsp, new_root_dir); + arr_add(w->added, document_id); + lsp_send_request(lsp, &req); + // *technically* this is incorrect because if the server *just now sent* a + // workspace/workspaceFolders request, we'd give it back inconsistent information. + // i don't care. + SDL_LockMutex(lsp->workspace_folders_mutex); + arr_add(lsp->workspace_folders, document_id); + SDL_UnlockMutex(lsp->workspace_folders_mutex); + return true; +} + bool lsp_next_message(LSP *lsp, LSPMessage *message) { bool any = false; SDL_LockMutex(lsp->messages_mutex); @@ -362,6 +412,8 @@ void lsp_free(LSP *lsp) { SDL_SemPost(lsp->quit_sem); SDL_WaitThread(lsp->communication_thread, NULL); SDL_DestroyMutex(lsp->messages_mutex); + SDL_DestroyMutex(lsp->workspace_folders_mutex); + SDL_DestroyMutex(lsp->error_mutex); SDL_DestroySemaphore(lsp->quit_sem); process_kill(&lsp->process); @@ -384,8 +436,6 @@ void lsp_free(LSP *lsp) { lsp_request_free(r); arr_free(lsp->requests_sent); - arr_foreach_ptr(lsp->workspace_folders, char *, folder) - free(*folder); arr_free(lsp->workspace_folders); arr_free(lsp->trigger_chars); @@ -26,12 +26,13 @@ typedef enum { // client-to-server LSP_REQUEST_INITIALIZE, LSP_REQUEST_INITIALIZED, + LSP_REQUEST_SHUTDOWN, + LSP_REQUEST_EXIT, LSP_REQUEST_DID_OPEN, LSP_REQUEST_DID_CLOSE, LSP_REQUEST_DID_CHANGE, LSP_REQUEST_COMPLETION, - LSP_REQUEST_SHUTDOWN, - LSP_REQUEST_EXIT, + LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS, // server-to-client LSP_REQUEST_SHOW_MESSAGE, @@ -98,17 +99,24 @@ typedef struct { } LSPRequestCompletion; typedef struct { + LSPDocumentID *removed; // dynamic array + LSPDocumentID *added; // dynamic array +} LSPRequestDidChangeWorkspaceFolders; + +typedef struct { // id is set by lsp.c; you shouldn't set it. u32 id; LSPRequestType type; char *id_string; // if not NULL, this is the ID (only for server-to-client messages; we always use integer IDs) - union { + // one member of this union is set depending on `type`. + union { LSPRequestDidOpen open; LSPRequestDidClose close; LSPRequestDidChange change; LSPRequestCompletion completion; - // for LSP_SHOW_MESSAGE and LSP_LOG_MESSAGE + // LSP_REQUEST_SHOW_MESSAGE or LSP_REQUEST_LOG_MESSAGE LSPRequestMessage message; + LSPRequestDidChangeWorkspaceFolders change_workspace_folders; } data; } LSPRequest; @@ -227,6 +235,7 @@ typedef struct { LSPCompletionItem *items; } LSPResponseCompletion; + typedef LSPRequestType LSPResponseType; typedef struct { LSPRequest request; // the request which this is a response to @@ -252,9 +261,19 @@ typedef struct { u32 version_number; // for LSP } LSPDocumentData; +typedef struct { + bool completion_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) + bool workspace_folders_support; +} LSPCapabilities; + typedef struct LSP { Process process; u32 request_id; + // for our purposes, folders are "documents" + // the spec kinda does this too: WorkspaceFolder has a `uri: DocumentUri` member. StrHashTable document_ids; // values are u32. they are indices into document_data. // this is a dynamic array which just keeps growing. // but the user isn't gonna open millions of files so it's fine. @@ -266,15 +285,18 @@ typedef struct LSP { // so that we can process responses. // this also lets us re-send requests if that's ever necessary. LSPRequest *requests_sent; - bool initialized; // has the response to the initialize request been sent? + // has the response to the initialize request been sent? + // we access this both in the main thread and in the LSP communication thread. + _Atomic bool initialized; SDL_Thread *communication_thread; SDL_sem *quit_sem; char *received_data; // dynamic array - bool provides_completion; // can this LSP server handle completion requests? + LSPCapabilities capabilities; char32_t *trigger_chars; // dynamic array of "trigger characters" SDL_mutex *error_mutex; Language language; - char **workspace_folders; // dynamic array of root directories of LSP "workspaces" (meaningless garbage) + SDL_mutex *workspace_folders_mutex; + LSPDocumentID *workspace_folders; // dynamic array of root directories of LSP "workspaces" (meaningless garbage) char error[256]; } LSP; @@ -293,6 +315,11 @@ void lsp_send_request(LSP *lsp, LSPRequest *request); void lsp_send_response(LSP *lsp, LSPResponse *response); const char *lsp_response_string(const LSPResponse *response, LSPString string); LSP *lsp_create(const char *root_dir, Language language, const char *analyzer_command); +// try to add a new "workspace folder" to the lsp. +// 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); bool lsp_next_message(LSP *lsp, LSPMessage *message); void lsp_document_changed(LSP *lsp, const char *document, LSPDocumentChangeEvent change); void lsp_free(LSP *lsp); @@ -1,6 +1,11 @@ /* @TODO: - ignore telemetry/event +- https://github.com/typescript-language-server/typescript-language-server + - NOTE: This supports javascript. + - We should also add a typescript language (but just use javascript syntax highlighting for now) + - (don't want to add .ts as a Javascript extension since that won't be forwards-compatible + if we ever do add real typescript highlighting support) - handle window/showMessageRequest - make sure "save as" works - what's wrong with gopls? @@ -8,6 +13,7 @@ - hover - go to definition using LSP - find usages + - that thing where it shows you the current function argument - workspaceFolders support (so we don't need to start up multiple instances of rust-analyzer) - document lsp.h and lsp.c. - maximum queue size for requests/responses just in case? @@ -39,6 +45,7 @@ FUTURE FEATURES: - keyboard macros - ctrl+9/0 to inc/dec number would be useful here - with macros we can really test performance of buffer_insert_text_at_pos, etc. (which should ideally be fast) +- real typescript highlighting (?) */ #include "base.h" @@ -48,15 +48,19 @@ static void *ted_realloc(Ted *ted, void *p, size_t new_size) { return ret; } +char *ted_get_root_dir_of(Ted *ted, const char *path) { + Settings *settings = ted_active_settings(ted); + return settings_get_root_dir(settings, path); +} + // get the project root directory (based on the active buffer or ted->cwd if there's no active buffer). // the return value should be freed char *ted_get_root_dir(Ted *ted) { - Settings *settings = ted_active_settings(ted); TextBuffer *buffer = ted->active_buffer; if (buffer) { - return settings_get_root_dir(settings, buffer->filename); + return ted_get_root_dir_of(ted, buffer->filename); } else { - return settings_get_root_dir(settings, ted->cwd); + return ted_get_root_dir_of(ted, ted->cwd); } } @@ -91,6 +95,14 @@ Settings *ted_get_settings(Ted *ted, const char *path, Language language) { return settings; } +// IMPORTANT NOTE ABOUT CACHING LSPs: +// - be careful if you want to cache LSPs, e.g. adding a LSP *lsp member to `buffer`. +// - right now we pretend that the server has workspace folder support until the initialize response is sent. +// - specifically, this means that: +// ted_get_lsp("/path1/a") => new LSP 0x12345 +// ted_get_lsp("/path2/b") => same LSP 0x12345 +// (receive initialize request, realize we don't have workspace folder support) +// ted_get_lsp("/path2/b") => new LSP 0x6789A LSP *ted_get_lsp(Ted *ted, const char *path, Language language) { Settings *settings = ted_get_settings(ted, path, language); if (!settings->lsp_enabled) @@ -102,11 +114,11 @@ LSP *ted_get_lsp(Ted *ted, const char *path, Language language) { if (!lsp) break; if (lsp->language != language) continue; - // check if root matches up - arr_foreach_ptr(lsp->workspace_folders, char *, lsp_root) { - if (str_has_path_prefix(path, *lsp_root)) - return lsp; - } + // check if root matches up or if we can add a workspace folder + char *root = ted_get_root_dir_of(ted, path); + bool success = lsp_try_add_root_dir(lsp, root); + free(root); + if (success) return lsp; } if (i == TED_LSP_MAX) return NULL; // why are there so many LSP open??? @@ -51,7 +51,7 @@ regenerate-tags-if-not-found = yes # So if /a/b/.git and /a/Makefile and /a/b/c/Cargo.toml exist, # ted will select /a/b as the root. # if no identifying files are found, the directory containing the current file is used. -root-identifiers = .ted-root.out, Cargo.toml, make.bat, CMakeLists.txt, Makefile, go.mod, .git +root-identifiers = .ted-root, .ted-root.out, Cargo.toml, make.bat, CMakeLists.txt, Makefile, go.mod, .git # you can make your own custom background for ted using a shader. # an example is provided here. you will have access to the following variables: diff --git a/test/lsp/JavaA/.ted-root b/test/lsp/JavaA/.ted-root new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/lsp/JavaA/.ted-root diff --git a/test/lsp/JavaA/Main.java b/test/lsp/JavaA/Main.java new file mode 100644 index 0000000..2792b7c --- /dev/null +++ b/test/lsp/JavaA/Main.java @@ -0,0 +1,8 @@ +public class Main { + public static void main(String[] args) { + int x = 0; + x++; + for (int i = 0; i < args.length; ++i) { + } + } +} diff --git a/test/lsp/JavaB/.ted-root b/test/lsp/JavaB/.ted-root new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/lsp/JavaB/.ted-root diff --git a/test/lsp/JavaB/B.java b/test/lsp/JavaB/B.java new file mode 100644 index 0000000..fde8107 --- /dev/null +++ b/test/lsp/JavaB/B.java @@ -0,0 +1,13 @@ +public class B { + private static class Something { + public int x = 0; + int f() { + return x; + } + } + + public static void main(String[] args) { + Something s = new Something(); + s.f(); + } +} diff --git a/test.java b/test/test.java index 7183aed..7183aed 100644 --- a/test.java +++ b/test/test.java |