From 0dcfd5a4f1fd865c24c01b17b214e1f72e4c06fe Mon Sep 17 00:00:00 2001 From: pommicket Date: Fri, 8 Sep 2023 22:59:47 -0400 Subject: LSP code formatting (not entirely working yet) --- CMakeLists.txt | 8 +++--- buffer.c | 25 +++++++++++++++++++ command.c | 8 ++++++ command.h | 2 ++ ide-format.c | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ide-rename-symbol.c | 8 +----- lsp-parse.c | 30 +++++++++++++++++++++++ lsp-write.c | 20 +++++++++++++++ lsp.c | 11 +++++++++ lsp.h | 29 +++++++++++++++++++--- main.c | 6 ++++- ted-internal.h | 20 +++++++++++++++ ted.c | 4 +-- ted.cfg | 7 ++++++ ted.h | 10 ++++++-- 15 files changed, 239 insertions(+), 19 deletions(-) create mode 100644 ide-format.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 652f407..238b113 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,10 +2,10 @@ cmake_minimum_required(VERSION 3.5) project(ted) if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(SOURCES buffer.c build.c colors.c command.c config.c find.c gl.c ide-autocomplete.c - ide-document-link.c ide-definitions.c ide-highlights.c ide-hover.c ide-signature-help.c - ide-usages.c ide-rename-symbol.c lsp.c lsp-json.c lsp-parse.c lsp-write.c main.c menu.c - node.c os.c session.c stb_image.c stb_truetype.c syntax.c tags.c ted.c text.c - ui.c util.c macro.c) + ide-document-link.c ide-definitions.c ide-format.c ide-highlights.c ide-hover.c + ide-signature-help.c ide-usages.c ide-rename-symbol.c lsp.c lsp-json.c lsp-parse.c + lsp-write.c main.c menu.c node.c os.c session.c stb_image.c stb_truetype.c syntax.c + tags.c ted.c text.c ui.c util.c macro.c) else() set(SOURCES main.c) endif() diff --git a/buffer.c b/buffer.c index 549a99e..1214fa4 100644 --- a/buffer.c +++ b/buffer.c @@ -1832,6 +1832,31 @@ LSPDocumentPosition buffer_cursor_pos_as_lsp_document_position(TextBuffer *buffe return buffer_pos_to_lsp_document_position(buffer, buffer->cursor_pos); } +LSPRange buffer_selection_as_lsp_range(TextBuffer *buffer) { + if (!buffer->selection) + return (LSPRange){0}; + LSPPosition cursor = buffer_pos_to_lsp_position(buffer, buffer->cursor_pos); + LSPPosition sel = buffer_pos_to_lsp_position(buffer, buffer->selection_pos); + if (buffer_pos_cmp(buffer->cursor_pos, buffer->selection_pos) < 0) { + return (LSPRange){ + .start = cursor, + .end = sel, + }; + } else { + return (LSPRange){ + .start = sel, + .end = cursor, + }; + } +} + +void buffer_apply_lsp_text_edit(TextBuffer *buffer, const LSPResponse *response, const LSPTextEdit *edit) { + BufferPos start = buffer_pos_from_lsp(buffer, edit->range.start); + BufferPos end = buffer_pos_from_lsp(buffer, edit->range.end); + buffer_delete_chars_between(buffer, start, end); + buffer_insert_utf8_at_pos(buffer, start, lsp_response_string(response, edit->new_text)); +} + static void buffer_send_lsp_did_change(LSP *lsp, TextBuffer *buffer, BufferPos pos, u32 nchars_deleted, String32 new_text) { if (!buffer_is_named_file(buffer)) diff --git a/command.c b/command.c index 07d3856..2407551 100644 --- a/command.c +++ b/command.c @@ -105,6 +105,8 @@ static CommandName command_names[] = { {"increment-number", CMD_INCREMENT_NUMBER}, {"decrement-number", CMD_DECREMENT_NUMBER}, {"rename-symbol", CMD_RENAME_SYMBOL}, + {"format-file", CMD_FORMAT_FILE}, + {"format-selection", CMD_FORMAT_SELECTION}, }; static_assert_if_possible(arr_count(command_names) == CMD_COUNT) @@ -703,5 +705,11 @@ void command_execute_ex(Ted *ted, Command c, const CommandArgument *full_argumen if (buffer && buffer_lsp(buffer)) menu_open(ted, MENU_RENAME_SYMBOL); break; + case CMD_FORMAT_FILE: + format_file(ted); + break; + case CMD_FORMAT_SELECTION: + format_selection(ted); + break; } } diff --git a/command.h b/command.h index 25ba4a5..1beb18a 100644 --- a/command.h +++ b/command.h @@ -112,6 +112,8 @@ typedef enum { CMD_GOTO_DEFINITION_AT_CURSOR, CMD_GOTO_DECLARATION_AT_CURSOR, CMD_GOTO_TYPE_DEFINITION_AT_CURSOR, + CMD_FORMAT_FILE, + CMD_FORMAT_SELECTION, CMD_LSP_RESET, CMD_COPY, diff --git a/ide-format.c b/ide-format.c new file mode 100644 index 0000000..70103c7 --- /dev/null +++ b/ide-format.c @@ -0,0 +1,70 @@ +#include "ted-internal.h" + +struct Formatting { + LSPServerRequestID last_request_id; +}; + +void format_init(Ted *ted) { + ted->formatting = calloc(1, sizeof (Formatting)); +} + +static void format_common(Ted *ted, bool selection) { + Formatting *formatting = ted->formatting; + ted_cancel_lsp_request(ted, &formatting->last_request_id); + TextBuffer *buffer = ted_active_buffer(ted); + if (!buffer) return; + if (selection && !buffer_has_selection(buffer)) return; + LSP *lsp = buffer_lsp(buffer); + if (!lsp) return; + Settings *settings = buffer_settings(buffer); + LSPRequest request = { + .type = selection ? LSP_REQUEST_RANGE_FORMATTING : LSP_REQUEST_FORMATTING + }; + LSPRequestFormatting *req_data = &request.data.formatting; + req_data->document = buffer_lsp_document_id(buffer); + req_data->indent_with_spaces = settings->indent_with_spaces; + req_data->tab_width = settings->tab_width; + if (selection) { + req_data->use_range = true; + req_data->range = buffer_selection_as_lsp_range(buffer); + } + formatting->last_request_id = lsp_send_request(lsp, &request); +} + +void format_selection(Ted *ted) { + format_common(ted, true); +} + +void format_file(Ted *ted) { + format_common(ted, false); +} + +void format_cancel_request(Ted *ted) { + Formatting *formatting = ted->formatting; + ted_cancel_lsp_request(ted, &formatting->last_request_id); +} + +void format_process_lsp_response(Ted *ted, const LSPResponse *response) { + Formatting *formatting = ted->formatting; + const LSPRequest *request = &response->request; + if (request->id != formatting->last_request_id.id) + return; + if (!(request->type == LSP_REQUEST_RANGE_FORMATTING + || request->type == LSP_REQUEST_FORMATTING)) { + return; + } + TextBuffer *buffer = ted->active_buffer; + if (!buffer) return; + if (buffer_lsp_document_id(buffer) != request->data.formatting.document) + return; // switched document + + const LSPResponseFormatting *f = &response->data.formatting; + arr_foreach_ptr(f->edits, const LSPTextEdit, edit) { + buffer_apply_lsp_text_edit(buffer, response, edit); + } +} + +void format_quit(Ted *ted) { + free(ted->formatting); + ted->formatting = NULL; +} diff --git a/ide-rename-symbol.c b/ide-rename-symbol.c index b5208cc..c32f984 100644 --- a/ide-rename-symbol.c +++ b/ide-rename-symbol.c @@ -152,13 +152,7 @@ void rename_symbol_process_lsp_response(Ted *ted, const LSPResponse *response) { goto done; } - - const LSPTextEdit *edit = &change_data->edit; - BufferPos start = buffer_pos_from_lsp(buffer, edit->range.start); - BufferPos end = buffer_pos_from_lsp(buffer, edit->range.end); - buffer_delete_chars_between(buffer, start, end); - buffer_insert_utf8_at_pos(buffer, start, lsp_response_string(response, edit->new_text)); - + buffer_apply_lsp_text_edit(buffer, response, &change_data->edit); } break; case LSP_CHANGE_RENAME: { diff --git a/lsp-parse.c b/lsp-parse.c index 70301d3..61e44c4 100644 --- a/lsp-parse.c +++ b/lsp-parse.c @@ -234,6 +234,16 @@ static void parse_capabilities(LSP *lsp, const JSON *json, JSONObject capabiliti if (workspace_symbol_value.type != JSON_UNDEFINED && workspace_symbol_value.type != JSON_FALSE) { cap->workspace_symbols_support = true; } + + JSONValue formatting_value = json_object_get(json, capabilities, "documentFormattingProvider"); + if (formatting_value.type == JSON_OBJECT || formatting_value.type == JSON_TRUE) { + cap->formatting_support = true; + } + + JSONValue range_formatting_value = json_object_get(json, capabilities, "documentRangeFormattingProvider"); + if (range_formatting_value.type == JSON_OBJECT || range_formatting_value.type == JSON_TRUE) { + cap->range_formatting_support = true; + } JSONObject workspace = json_object_get_object(json, capabilities, "workspace"); // check WorkspaceFoldersServerCapabilities @@ -993,6 +1003,22 @@ static bool parse_document_link_response(LSP *lsp, const JSON *json, LSPResponse return true; } +static bool parse_formatting_response(LSP *lsp, const JSON *json, LSPResponse *response) { + JSONValue edits_val = json_get(json, "result"); + if (!(edits_val.type == JSON_ARRAY || edits_val.type == JSON_NULL)) { + lsp_set_error(lsp, "Expected TextEdit[] or null for formatting response; got %s", + json_type_to_str(edits_val.type)); + return false; + } + JSONArray edits = json_force_array(edits_val); + LSPResponseFormatting *f = &response->data.formatting; + for (u32 i = 0; i < edits.len; ++i) { + if (!parse_text_edit(lsp, response, json, json_array_get(json, edits, i), arr_addp(f->edits))) + return false; + } + return true; +} + void process_message(LSP *lsp, JSON *json) { #if 0 @@ -1066,6 +1092,10 @@ void process_message(LSP *lsp, JSON *json) { case LSP_REQUEST_RENAME: add_to_messages = parse_rename_response(lsp, json, &response); break; + case LSP_REQUEST_FORMATTING: + case LSP_REQUEST_RANGE_FORMATTING: + add_to_messages = parse_formatting_response(lsp, json, &response); + break; case LSP_REQUEST_DOCUMENT_LINK: add_to_messages = parse_document_link_response(lsp, json, &response); break; diff --git a/lsp-write.c b/lsp-write.c index 17aef7f..050d784 100644 --- a/lsp-write.c +++ b/lsp-write.c @@ -302,6 +302,10 @@ static const char *lsp_request_method(LSPRequest *request) { return "workspace/didChangeConfiguration"; case LSP_REQUEST_WORKSPACE_SYMBOLS: return "workspace/symbol"; + case LSP_REQUEST_RANGE_FORMATTING: + return "textDocument/rangeFormatting"; + case LSP_REQUEST_FORMATTING: + return "textDocument/formatting"; } assert(0); return "$/ignore"; @@ -652,6 +656,22 @@ void write_request(LSP *lsp, LSPRequest *request) { str_builder_append(&o->builder, lsp_request_string(request, config->settings)); write_obj_end(o); } break; + case LSP_REQUEST_FORMATTING: + case LSP_REQUEST_RANGE_FORMATTING: { + const LSPRequestFormatting *formatting = &request->data.formatting; + write_key_obj_start(o, "params"); + write_key_obj_start(o, "textDocument"); + write_key_file_uri(o, "uri", formatting->document); + write_obj_end(o); + write_key_obj_start(o, "options"); + write_key_number(o, "tabSize", formatting->tab_width); + write_key_bool(o, "insertSpaces", formatting->indent_with_spaces); + write_obj_end(o); + if (formatting->use_range) { + write_key_range(o, "range", formatting->range); + } + write_obj_end(o); + } break; } write_obj_end(o); diff --git a/lsp.c b/lsp.c index 9d98b8c..0ab78a8 100644 --- a/lsp.c +++ b/lsp.c @@ -119,6 +119,8 @@ void lsp_request_free(LSPRequest *r) { case LSP_REQUEST_DOCUMENT_LINK: case LSP_REQUEST_CONFIGURATION: case LSP_REQUEST_DID_OPEN: + case LSP_REQUEST_FORMATTING: + case LSP_REQUEST_RANGE_FORMATTING: break; case LSP_REQUEST_PUBLISH_DIAGNOSTICS: { LSPRequestPublishDiagnostics *pub = &r->data.publish_diagnostics; @@ -169,6 +171,9 @@ void lsp_response_free(LSPResponse *r) { case LSP_REQUEST_DOCUMENT_LINK: arr_free(r->data.document_link.links); break; + case LSP_REQUEST_FORMATTING: + arr_free(r->data.formatting.edits); + break; default: break; } @@ -252,6 +257,10 @@ static bool lsp_supports_request(LSP *lsp, const LSPRequest *request) { return cap->references_support; case LSP_REQUEST_DOCUMENT_LINK: return cap->document_link_support; + case LSP_REQUEST_FORMATTING: + return cap->formatting_support; + case LSP_REQUEST_RANGE_FORMATTING: + return cap->range_formatting_support; } assert(0); return false; @@ -293,6 +302,8 @@ static bool request_type_is_notification(LSPRequestType type) { case LSP_REQUEST_WORKSPACE_SYMBOLS: case LSP_REQUEST_WORKSPACE_FOLDERS: case LSP_REQUEST_DOCUMENT_LINK: + case LSP_REQUEST_FORMATTING: + case LSP_REQUEST_RANGE_FORMATTING: return false; } assert(0); diff --git a/lsp.h b/lsp.h index 5545c61..832d3d4 100644 --- a/lsp.h +++ b/lsp.h @@ -87,9 +87,10 @@ typedef enum { LSP_REQUEST_REFERENCES, //< textDocument/references LSP_REQUEST_RENAME, //< textDocument/rename LSP_REQUEST_DOCUMENT_LINK, //< textDocument/documentLink + LSP_REQUEST_FORMATTING, //< textDocument/formatting + LSP_REQUEST_RANGE_FORMATTING, //< textDocument/rangeFormatting LSP_REQUEST_WORKSPACE_SYMBOLS, //< workspace/symbol LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS, //< workspace/didChangeWorkspaceFolders - // server-to-client LSP_REQUEST_SHOW_MESSAGE, //< window/showMessage and window/showMessageRequest LSP_REQUEST_LOG_MESSAGE, //< window/logMessage @@ -239,6 +240,17 @@ typedef struct { LSPString settings; } LSPRequestConfiguration; +typedef struct { + LSPDocumentID document; + u8 tab_width; + bool indent_with_spaces; + bool use_range; + /// range to format + /// + /// only applicable if `use_range` is `true`. + LSPRange range; +} LSPRequestFormatting; + typedef struct { LSPMessageType type; /// LSP requests/responses tend to have a lot of strings. @@ -247,6 +259,7 @@ typedef struct { char *string_data; } LSPMessageBase; +/// an LSP request or notification typedef struct { LSPMessageBase base; LSPRequestID id; @@ -273,6 +286,8 @@ typedef struct { LSPRequestRename rename; LSPRequestDocumentLink document_link; LSPRequestPublishDiagnostics publish_diagnostics; + // LSP_REQUEST_FORMATTING and LSP_REQUEST_RANGE_FORMATTING + LSPRequestFormatting formatting; } data; } LSPRequest; @@ -524,6 +539,10 @@ typedef struct { LSPDocumentLink *links; } LSPResponseDocumentLink; +typedef struct { + LSPTextEdit *edits; +} LSPResponseFormatting; + typedef struct { LSPMessageBase base; /// the request which this is a response to @@ -535,13 +554,15 @@ typedef struct { LSPResponseCompletion completion; LSPResponseSignatureHelp signature_help; LSPResponseHover hover; - // LSP_REQUEST_DEFINITION, LSP_REQUEST_DECLARATION, LSP_REQUEST_TYPE_DEFINITION, or LSP_REQUEST_IMPLEMENTATION + /// `LSP_REQUEST_DEFINITION`, `LSP_REQUEST_DECLARATION`, `LSP_REQUEST_TYPE_DEFINITION`, or `LSP_REQUEST_IMPLEMENTATION` LSPResponseDefinition definition; LSPResponseWorkspaceSymbols workspace_symbols; LSPResponseRename rename; LSPResponseHighlight highlight; LSPResponseReferences references; LSPResponseDocumentLink document_link; + /// `LSP_REQUEST_FORMATTING` or `LSP_REQUEST_RANGE_FORMATTING` + LSPResponseFormatting formatting; } data; } LSPResponse; @@ -576,6 +597,8 @@ typedef struct { bool rename_support; bool references_support; bool document_link_support; + bool formatting_support; + bool range_formatting_support; } LSPCapabilities; typedef struct LSP { @@ -596,7 +619,7 @@ typedef struct LSP { Process *process; // Socket for communicating with server. Maybe be NULL if communication is done over stdio. // - // thread-safety: TODO + // thread-safety: only accessed in communication thread // at least one of `process` and `socket` must be non-null Socket *socket; // port used for communication diff --git a/main.c b/main.c index acbeaa2..a0b6e67 100644 --- a/main.c +++ b/main.c @@ -1,7 +1,7 @@ /* TODO: +- figure out what's wrong with format-selection with clangd - figure out what's wrong with godot language server -- LSP textDocument/formatting and textDocument/rangeFormatting - automatically restart server FUTURE FEATURES: - autodetect indentation (tabs vs spaces) @@ -77,6 +77,7 @@ FUTURE FEATURES: #include "ide-highlights.c" #include "ide-usages.c" #include "ide-document-link.c" +#include "ide-format.c" #include "command.c" #include "macro.c" #include "config.c" @@ -503,6 +504,7 @@ int main(int argc, char **argv) { macros_init(ted); definitions_init(ted); autocomplete_init(ted); + format_init(ted); signature_help_init(ted); usages_init(ted); highlights_init(ted); @@ -900,6 +902,7 @@ int main(int argc, char **argv) { // it's important that we send error responses here too. // we don't want to be waiting around for a response that's never coming. autocomplete_process_lsp_response(ted, r); + format_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); @@ -1198,6 +1201,7 @@ int main(int argc, char **argv) { hover_quit(ted); signature_help_quit(ted); autocomplete_quit(ted); + format_quit(ted); highlights_quit(ted); usages_quit(ted); session_write(ted); diff --git a/ted-internal.h b/ted-internal.h index a9ee175..6868895 100644 --- a/ted-internal.h +++ b/ted-internal.h @@ -208,6 +208,9 @@ typedef struct Autocomplete Autocomplete; /// data needed for finding usages typedef struct Usages Usages; +/// data needed for formatting code +typedef struct Formatting Formatting; + /// max number of signatures to display at a time. #define SIGNATURE_HELP_MAX 5 @@ -340,6 +343,7 @@ struct Ted { Highlights *highlights; Usages *usages; RenameSymbol *rename_symbol; + Formatting *formatting; FILE *log; @@ -450,6 +454,12 @@ LSPDocumentPosition buffer_pos_to_lsp_document_position(TextBuffer *buffer, Buff BufferPos buffer_pos_from_lsp(TextBuffer *buffer, LSPPosition lsp_pos); /// Get the cursor position as an LSPPosition. LSPPosition buffer_cursor_pos_as_lsp_position(TextBuffer *buffer); +/// Get the current selection as an LSPRange. +/// +/// Returns `(LSPRange){0}` if nothing is selected. +LSPRange buffer_selection_as_lsp_range(TextBuffer *buffer); +/// Apply LSP TextEdit from response +void buffer_apply_lsp_text_edit(TextBuffer *buffer, const LSPResponse *response, const LSPTextEdit *edit); /// Get the cursor position as an LSPDocumentPosition. LSPDocumentPosition buffer_cursor_pos_as_lsp_document_position(TextBuffer *buffer); /// highlight an \ref LSPRange in this buffer. @@ -617,6 +627,16 @@ void document_link_quit(Ted *ted); void document_link_frame(Ted *ted); void document_link_process_lsp_response(Ted *ted, const LSPResponse *response); + +// === ide-format.c === +/// initialize formatting stuff +void format_init(Ted *ted); +void format_process_lsp_response(Ted *ted, const LSPResponse *response); +/// cancel last formatting request +void format_cancel_request(Ted *ted); +/// free formatting stuff +void format_quit(Ted *ted); + // === ide-highlights.c === void highlights_init(Ted *ted); void highlights_quit(Ted *ted); diff --git a/ted.c b/ted.c index a21c317..5167900 100644 --- a/ted.c +++ b/ted.c @@ -51,11 +51,11 @@ static void ted_vset_message(Ted *ted, MessageType type, const char *fmt, va_lis } } -TextBuffer *ted_get_active_buffer(Ted *ted) { +TextBuffer *ted_active_buffer(Ted *ted) { return ted->active_buffer; } -TextBuffer *ted_get_active_buffer_behind_menu(Ted *ted) { +TextBuffer *ted_active_buffer_behind_menu(Ted *ted) { return ted->prev_active_buffer; } diff --git a/ted.cfg b/ted.cfg index 6b9aaf7..dd36a1f 100644 --- a/ted.cfg +++ b/ted.cfg @@ -334,6 +334,13 @@ Ctrl+x = :cut Ctrl+v = :paste Ctrl+Shift+p = :command-selector +# use cargo fmt or clang-tidy or whatever the LSP server +# decides to use to format the current selection/file. +# by default no keybinding is set so you don't accidentally +# format a whole file. +# Ctrl+Alt+f = :format-selection +# Ctrl+Alt+Shift+f = :format-file + Ctrl+9 = :decrement-number Ctrl+0 = :increment-number diff --git a/ted.h b/ted.h index b1c8950..984ab70 100644 --- a/ted.h +++ b/ted.h @@ -921,6 +921,12 @@ void definition_cancel_lookup(Ted *ted); /// so don't keep it around long. const char *document_link_at_buffer_pos(Ted *ted, BufferPos pos); +// === ide-format.c === +/// format current selection (using LSP server) +void format_selection(Ted *ted); +/// format current file (using LSP server) +void format_file(Ted *ted); + // === ide-highlights.c === // === ide-hover.c === @@ -1094,11 +1100,11 @@ bool tag_goto(Ted *ted, const char *tag); /// for fatal errors void die(PRINTF_FORMAT_STRING const char *fmt, ...) ATTRIBUTE_PRINTF(1, 2); /// returns the current active buffer, or `NULL` if no buffer is active. -TextBuffer *ted_get_active_buffer(Ted *ted); +TextBuffer *ted_active_buffer(Ted *ted); /// if a menu is open, returns the buffer that was open before the menu was opened. /// /// returns `NULL` if no menu is open or no buffer was open before the menu was opened. -TextBuffer *ted_get_active_buffer_behind_menu(Ted *ted); +TextBuffer *ted_active_buffer_behind_menu(Ted *ted); /// get width of ted window float ted_window_width(Ted *ted); /// get height of ted window -- cgit v1.2.3