diff options
-rw-r--r-- | buffer.c | 144 | ||||
-rw-r--r-- | lsp.c | 277 | ||||
-rw-r--r-- | lsp.h | 180 | ||||
-rw-r--r-- | main.c | 18 | ||||
-rw-r--r-- | ted.c | 5 | ||||
-rw-r--r-- | ted.h | 3 | ||||
-rw-r--r-- | unicode.h | 2 |
7 files changed, 338 insertions, 291 deletions
@@ -2040,6 +2040,9 @@ void buffer_paste(TextBuffer *buffer) { } } +LSP *buffer_lsp(TextBuffer *buffer) { + return ted_get_lsp(buffer->ted, buffer_language(buffer)); +} // if an error occurs, buffer is left untouched (except for the error field) and the function returns false. Status buffer_load_file(TextBuffer *buffer, char const *filename) { @@ -2060,87 +2063,92 @@ Status buffer_load_file(TextBuffer *buffer, char const *filename) { success = false; } else { u8 *file_contents = buffer_calloc(buffer, 1, file_size); - if (file_contents) { - lines_capacity = 4; - lines = buffer_calloc(buffer, lines_capacity, sizeof *buffer->lines); // initial lines - if (lines) { - nlines = 1; - size_t bytes_read = fread(file_contents, 1, file_size, fp); - if (bytes_read == file_size) { - char32_t c = 0; - for (u8 *p = file_contents, *end = p + file_size; p != end; ) { - if (*p == '\r' && p != end-1 && p[1] == '\n') { - // CRLF line endings - p += 2; - c = '\n'; - } else { - size_t n = unicode_utf8_to_utf32(&c, (char *)p, (size_t)(end - p)); - if (n == 0) { - // null character - c = 0; - ++p; - } else if (n >= (size_t)(-2)) { - // invalid UTF-8 - success = false; - buffer_seterr(buffer, "Invalid UTF-8 (position: %td).", p - file_contents); - break; - } else { - p += n; - } - } - if (c == '\n') { - if (buffer_lines_set_min_capacity(buffer, &lines, &lines_capacity, nlines + 1)) - ++nlines; - } else { - u32 line_idx = nlines - 1; - Line *line = &lines[line_idx]; - buffer_line_append_char(buffer, line, c); - } - } + lines_capacity = 4; + lines = buffer_calloc(buffer, lines_capacity, sizeof *buffer->lines); // initial lines + nlines = 1; + size_t bytes_read = fread(file_contents, 1, file_size, fp); + if (bytes_read == file_size) { + char32_t c = 0; + for (u8 *p = file_contents, *end = p + file_size; p != end; ) { + if (*p == '\r' && p != end-1 && p[1] == '\n') { + // CRLF line endings + p += 2; + c = '\n'; } else { - success = false; + size_t n = unicode_utf8_to_utf32(&c, (char *)p, (size_t)(end - p)); + if (n == 0) { + // null character + c = 0; + ++p; + } else if (n >= (size_t)(-2)) { + // invalid UTF-8 + success = false; + buffer_seterr(buffer, "Invalid UTF-8 (position: %td).", p - file_contents); + break; + } else { + p += n; + } } - if (!success) { - // something went wrong; we need to free all the memory we used - for (u32 i = 0; i < nlines; ++i) - buffer_line_free(&lines[i]); - free(lines); + if (c == '\n') { + if (buffer_lines_set_min_capacity(buffer, &lines, &lines_capacity, nlines + 1)) + ++nlines; + } else { + u32 line_idx = nlines - 1; + Line *line = &lines[line_idx]; + buffer_line_append_char(buffer, line, c); } } - free(file_contents); } - if (ferror(fp)) { + if (ferror(fp)) { buffer_seterr(buffer, "Error reading from file."); success = false; } + if (!success) { + // something went wrong; we need to free all the memory we used + for (u32 i = 0; i < nlines; ++i) + buffer_line_free(&lines[i]); + free(lines); + } + + if (success) { + char *filename_copy = buffer_strdup(buffer, filename); + if (!filename_copy) success = false; + if (success) { + // everything is good + buffer_clear(buffer); + buffer->lines = lines; + buffer->nlines = nlines; + buffer->frame_earliest_line_modified = 0; + buffer->frame_latest_line_modified = nlines - 1; + buffer->lines_capacity = lines_capacity; + buffer->filename = filename_copy; + buffer->last_write_time = time_last_modified(buffer->filename); + if (!(fs_path_permission(filename) & FS_PERMISSION_WRITE)) { + // can't write to this file; make the buffer view only. + buffer->view_only = true; + } + } + + LSP *lsp = buffer_lsp(buffer); + if (lsp) { + // send didOpen + LSPRequest request = {.type = LSP_OPEN}; + LSPRequestOpen *open = &request.data.open; + open->file_contents = (char *)file_contents; + open->path = str_dup(filename); + open->language = buffer_language(buffer); + lsp_send_request(lsp, &request); + file_contents = NULL; // don't free + } + } + + free(file_contents); } - if (fclose(fp) != 0) { - buffer_seterr(buffer, "Error closing file."); - success = false; - } + fclose(fp); } else { buffer_seterr(buffer, "Couldn't open file %s: %s.", filename, strerror(errno)); success = false; } - if (success) { - char *filename_copy = buffer_strdup(buffer, filename); - if (!filename_copy) success = false; - if (success) { - // everything is good - buffer_clear(buffer); - buffer->lines = lines; - buffer->nlines = nlines; - buffer->frame_earliest_line_modified = 0; - buffer->frame_latest_line_modified = nlines - 1; - buffer->lines_capacity = lines_capacity; - buffer->filename = filename_copy; - buffer->last_write_time = time_last_modified(buffer->filename); - if (!(fs_path_permission(filename) & FS_PERMISSION_WRITE)) { - // can't write to this file; make the buffer view only. - buffer->view_only = true; - } - } - } return success; } @@ -1,174 +1,3 @@ -// @TODO: -// - document this file. maybe make lsp.h -// - maximum queue size for requests/responses just in case? -// - delete old LSPRequestTrackedInfos -// (if the server never sends a response) -// - TESTING: make rust-analyzer-slow (waits 10s before sending response) - -typedef enum { - LSP_REQUEST, - LSP_RESPONSE -} LSPMessageType; - -typedef enum { - LSP_NONE, - - // client-to-server - LSP_INITIALIZE, - LSP_INITIALIZED, - LSP_OPEN, - LSP_COMPLETION, - LSP_SHUTDOWN, - LSP_EXIT, - - // server-to-client - LSP_SHOW_MESSAGE, - LSP_LOG_MESSAGE -} LSPRequestType; - -typedef struct { - // buffer language - Language language; - // freed by lsp_request_free - char *filename; - // freed by lsp_request_free - char *file_contents; -} LSPRequestOpen; - -typedef enum { - ERROR = 1, - WARNING = 2, - INFO = 3, - LOG = 4 -} LSPWindowMessageType; - -typedef struct { - LSPWindowMessageType type; - // freed by lsp_request_free - char *message; -} LSPRequestMessage; - -typedef struct { - // freed by lsp_request_free - char *path; - u32 line; - // the **UTF-16** "character" offset within the line - u32 character; -} LSPDocumentPosition; - -typedef struct { - LSPDocumentPosition position; -} LSPRequestCompletion; - -typedef struct { - LSPRequestType type; - union { - LSPRequestOpen open; - LSPRequestCompletion completion; - // for LSP_SHOW_MESSAGE and LSP_LOG_MESSAGE - LSPRequestMessage message; - } data; -} LSPRequest; - -// info we want to keep track of about a request, -// so we can deal with the response appropriately. -typedef struct { - u64 id; - LSPRequestType type; -} LSPRequestTrackedInfo; - -typedef struct { - u32 offset; -} LSPString; - -typedef struct { - u32 line; - u32 character; -} LSPPosition; - -typedef struct { - LSPPosition start; - LSPPosition end; -} LSPRange; - -// see InsertTextFormat in the LSP spec. -typedef enum { - // plain text - LSP_TEXT_EDIT_PLAIN = 1, - // snippet e.g. "some_method($1, $2)$0" - LSP_TEXT_EDIT_SNIPPET = 2 -} LSPTextEditType; - -typedef struct { - LSPTextEditType type; - - // if set to true, `range` should be ignored - // -- this is a completion which uses insertText. - // how to handle this: - // "VS Code when code complete is requested in this example - // `con<cursor position>` and a completion item with an `insertText` of - // `console` is provided it will only insert `sole`" - bool at_cursor; - - LSPRange range; - LSPString new_text; -} LSPTextEdit; - -typedef struct { - LSPString label; - LSPTextEdit text_edit; - // note: the items are sorted here in this file, - // so you probably don't need to access this. - LSPString sort_text; -} LSPCompletionItem; - -typedef struct { - // dynamic array - LSPCompletionItem *items; -} LSPResponseCompletion; - -typedef LSPRequestType LSPResponseType; -typedef struct { - LSPResponseType type; - // LSP responses tend to have a lot of strings. - // to avoid doing a ton of allocations+frees, - // they're all stored here. - char *string_data; - union { - LSPResponseCompletion completion; - } data; -} LSPResponse; - -typedef struct { - LSPMessageType type; - union { - LSPRequest request; - LSPResponse response; - } u; -} LSPMessage; - -typedef struct { - Process process; - u64 request_id; - LSPMessage *messages; - SDL_mutex *messages_mutex; - LSPRequest *requests_client2server; - LSPRequest *requests_server2client; - // only applicable for client-to-server requests - LSPRequestTrackedInfo *requests_tracked_info; - SDL_mutex *requests_mutex; - bool initialized; // has the response to the initialize request been sent? - SDL_Thread *communication_thread; - SDL_sem *quit_sem; - char *received_data; // dynamic array - SDL_mutex *error_mutex; - char error[256]; -} LSP; - -// 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 may 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) { bool has_err = false; SDL_LockMutex(lsp->error_mutex); @@ -227,8 +56,6 @@ static void lsp_position_free(LSPDocumentPosition *position) { static void lsp_request_free(LSPRequest *r) { switch (r->type) { case LSP_NONE: - assert(0); - break; case LSP_INITIALIZE: case LSP_INITIALIZED: case LSP_SHUTDOWN: @@ -240,7 +67,7 @@ static void lsp_request_free(LSPRequest *r) { } break; case LSP_OPEN: { LSPRequestOpen *open = &r->data.open; - free(open->filename); + free(open->path); free(open->file_contents); } break; case LSP_SHOW_MESSAGE: @@ -303,8 +130,9 @@ static WarnUnusedResult bool lsp_expect_number(LSP *lsp, JSONValue value, const // technically there are "requests" and "notifications" // notifications are different in that they don't have IDs and don't return responses. // this function handles both. +// NOTE: do not call lsp_request_free on request. freeing the request will be handled. // returns the ID of the request -static void write_request(LSP *lsp, const LSPRequest *request) { +static void write_request(LSP *lsp, LSPRequest *request) { StrBuilder builder = str_builder_new(); @@ -317,15 +145,9 @@ static void write_request(LSP *lsp, const LSPRequest *request) { bool is_notification = request->type == LSP_INITIALIZED || request->type == LSP_EXIT; if (!is_notification) { - unsigned long long id = lsp->request_id++; - str_builder_appendf(&builder, "\"id\":%llu,", id); - LSPRequestTrackedInfo info = { - .id = id, - .type = request->type - }; - SDL_LockMutex(lsp->requests_mutex); - arr_add(lsp->requests_tracked_info, info); - SDL_UnlockMutex(lsp->requests_mutex); + u32 id = lsp->request_id++; + request->id = id; + str_builder_appendf(&builder, "\"id\":%lu,", (unsigned long)id); } switch (request->type) { @@ -339,7 +161,9 @@ static void write_request(LSP *lsp, const LSPRequest *request) { str_builder_appendf(&builder, "\"method\":\"initialize\",\"params\":{" "\"processId\":%d," - "\"capabilities\":{}" + "\"capabilities\":{}," + "\"rootUri\":null," + "\"workspaceFolders\":null" "}", process_get_id()); } break; case LSP_INITIALIZED: @@ -347,21 +171,22 @@ static void write_request(LSP *lsp, const LSPRequest *request) { break; case LSP_OPEN: { const LSPRequestOpen *open = &request->data.open; - char *escaped_filename = json_escape(open->filename); + printf("text=%p\n",open->file_contents); + char *escaped_path = json_escape(open->path); char *escaped_text = json_escape(open->file_contents); - + // @TODO: escape directly into builder str_builder_appendf(&builder, - "\"method\":\"textDocument/open\",\"params\":{" - "textDocument:{" - "uri:\"file://%s\"," - "languageId:\"%s\"," - "version:1," - "text:\"%s\"}}", - escaped_filename, + "\"method\":\"textDocument/didOpen\",\"params\":{" + "\"textDocument\":{" + "\"uri\":\"file://%s\"," + "\"languageId\":\"%s\"," + "\"version\":1," + "\"text\":\"%s\"}}", + escaped_path, lsp_language_id(open->language), escaped_text); free(escaped_text); - free(escaped_filename); + free(escaped_path); } break; case LSP_COMPLETION: { const LSPRequestCompletion *completion = &request->data.completion; @@ -409,6 +234,14 @@ static void write_request(LSP *lsp, const LSPRequest *request) { process_write(&lsp->process, content, strlen(content)); str_builder_free(&builder); + + if (is_notification) { + lsp_request_free(request); + } else { + SDL_LockMutex(lsp->requests_mutex); + arr_add(lsp->requests_sent, *request); + SDL_UnlockMutex(lsp->requests_mutex); + } } // figure out if data begins with a complete LSP response. @@ -647,25 +480,41 @@ static void process_message(LSP *lsp, JSON *json) { json_debug_print(json); printf("\x1b[0m\n"); #endif - + JSONValue id_value = json_get(json, "id"); + + // get the request associated with this (if any) + LSPRequest response_to = {0}; + if (id_value.type == JSON_NUMBER) { + u64 id = (u64)id_value.val.number; + arr_foreach_ptr(lsp->requests_sent, LSPRequest, req) { + if (req->id == id) { + response_to = *req; + arr_remove(lsp->requests_sent, (u32)(req - lsp->requests_sent)); + break; + } + } + } + JSONValue error = json_get(json, "error.message"); if (error.type == JSON_STRING) { char err[256] = {0}; json_string_get(json, error.val.string, err, sizeof err);; - lsp_set_error(lsp, "%s", err); + + if (streq(err, "waiting for cargo metadata or cargo check")) { + // fine. be that way. i'll resend the goddamn request. + // i'll keep bombarding you with requests. + // maybe next time you should abide by the standard and only send an initialize response when youre actually ready to handle my requests. fuck you. + if (response_to.type) + lsp_send_request(lsp, &response_to); + } else { + lsp_set_error(lsp, "%s", err); + } goto ret; } JSONValue result = json_get(json, "result"); if (result.type != JSON_UNDEFINED) { - JSONValue id = json_get(json, "id"); - - if (id.type != JSON_NUMBER) { - // what - lsp_set_error(lsp, "Response with no ID."); - goto ret; - } - if (id.val.number == 0) { + if (response_to.type == LSP_INITIALIZE) { // it's the response to our initialize request! // let's send back an "initialized" request (notification) because apparently // that's something we need to do. @@ -677,22 +526,9 @@ static void process_message(LSP *lsp, JSON *json) { // we can now send requests which have nothing to do with initialization lsp->initialized = true; } else { - u64 id_no = (u64)id.val.number; - LSPRequestTrackedInfo tracked_info = {0}; - SDL_LockMutex(lsp->requests_mutex); - arr_foreach_ptr(lsp->requests_tracked_info, LSPRequestTrackedInfo, info) { - if (info->id == id_no) { - // hey its the thing - tracked_info = *info; - arr_remove(lsp->requests_tracked_info, (u32)(info - lsp->requests_tracked_info)); - break; - } - } - SDL_UnlockMutex(lsp->requests_mutex); - LSPResponse response = {0}; bool success = false; - switch (tracked_info.type) { + switch (response_to.type) { case LSP_COMPLETION: success = parse_completion(lsp, json, &response); break; @@ -723,6 +559,7 @@ static void process_message(LSP *lsp, JSON *json) { lsp_set_error(lsp, "Bad message from server (no result, no method)."); } ret: + lsp_request_free(&response_to); json_free(json); } @@ -804,7 +641,6 @@ static bool lsp_send(LSP *lsp) { // whatever. write_request(lsp, r); } - lsp_request_free(r); if (SDL_SemTryWait(lsp->quit_sem) == 0) { quit = true; @@ -902,6 +738,7 @@ void lsp_free(LSP *lsp) { SDL_SemPost(lsp->quit_sem); SDL_WaitThread(lsp->communication_thread, NULL); SDL_DestroyMutex(lsp->messages_mutex); + SDL_DestroyMutex(lsp->requests_mutex); SDL_DestroySemaphore(lsp->quit_sem); process_kill(&lsp->process); arr_free(lsp->received_data); @@ -0,0 +1,180 @@ +// @TODO: +// - document this and lsp.c. +// - maximum queue size for requests/responses just in case? +// - delete old sent requests +// (if the server never sends a response) +// - TESTING: make rust-analyzer-slow (waits 10s before sending response) + +typedef enum { + LSP_REQUEST, + LSP_RESPONSE +} LSPMessageType; + +typedef enum { + LSP_NONE, + + // client-to-server + LSP_INITIALIZE, + LSP_INITIALIZED, + LSP_OPEN, + LSP_COMPLETION, + LSP_SHUTDOWN, + LSP_EXIT, + + // server-to-client + LSP_SHOW_MESSAGE, + LSP_LOG_MESSAGE +} LSPRequestType; + +typedef struct { + // buffer language + Language language; + // freed by lsp_request_free + char *path; + // freed by lsp_request_free + char *file_contents; +} LSPRequestOpen; + +typedef enum { + ERROR = 1, + WARNING = 2, + INFO = 3, + LOG = 4 +} LSPWindowMessageType; + +typedef struct { + LSPWindowMessageType type; + // freed by lsp_request_free + char *message; +} LSPRequestMessage; + +typedef struct { + // freed by lsp_request_free + char *path; + u32 line; + // the **UTF-16** "character" offset within the line + u32 character; +} LSPDocumentPosition; + +typedef struct { + LSPDocumentPosition position; +} LSPRequestCompletion; + +typedef struct { + // id is set by lsp.c; you shouldn't set it. + u32 id; + LSPRequestType type; + union { + LSPRequestOpen open; + LSPRequestCompletion completion; + // for LSP_SHOW_MESSAGE and LSP_LOG_MESSAGE + LSPRequestMessage message; + } data; +} LSPRequest; + +typedef struct { + u32 offset; +} LSPString; + +typedef struct { + u32 line; + u32 character; +} LSPPosition; + +typedef struct { + LSPPosition start; + LSPPosition end; +} LSPRange; + +// see InsertTextFormat in the LSP spec. +typedef enum { + // plain text + LSP_TEXT_EDIT_PLAIN = 1, + // snippet e.g. "some_method($1, $2)$0" + LSP_TEXT_EDIT_SNIPPET = 2 +} LSPTextEditType; + +typedef struct { + LSPTextEditType type; + + // if set to true, `range` should be ignored + // -- this is a completion which uses insertText. + // how to handle this: + // "VS Code when code complete is requested in this example + // `con<cursor position>` and a completion item with an `insertText` of + // `console` is provided it will only insert `sole`" + bool at_cursor; + + LSPRange range; + LSPString new_text; +} LSPTextEdit; + +typedef struct { + LSPString label; + LSPTextEdit text_edit; + // note: the items are sorted here in this file, + // so you probably don't need to access this. + LSPString sort_text; +} LSPCompletionItem; + +typedef struct { + // dynamic array + LSPCompletionItem *items; +} LSPResponseCompletion; + +typedef LSPRequestType LSPResponseType; +typedef struct { + LSPResponseType type; + // LSP responses tend to have a lot of strings. + // to avoid doing a ton of allocations+frees, + // they're all stored here. + char *string_data; + union { + LSPResponseCompletion completion; + } data; +} LSPResponse; + +typedef struct { + LSPMessageType type; + union { + LSPRequest request; + LSPResponse response; + } u; +} LSPMessage; + +typedef struct LSP { + Process process; + u32 request_id; + LSPMessage *messages; + SDL_mutex *messages_mutex; + LSPRequest *requests_client2server; + LSPRequest *requests_server2client; + // we keep track of client-to-server requests + // so that we can process responses. + // also fucking rust-analyzer gives "waiting for cargo metadata or cargo check" + // WHY NOT JUST WAIT UNTIL YOUVE DONE THAT BEFORE SENDING THE INITIALIZE RESPONSE. YOU HAVE NOT FINISHED INITIALIZATION. YOU ARE LYING. + // YOU GIVE A -32801 ERROR CODE WHICH IS "ContentModified" -- WHAT THE FUCK? THATS JUST COMPLETLY WRONG + // so we need to re-send requests in that case. + LSPRequest *requests_sent; + SDL_mutex *requests_mutex; + bool initialized; // has the response to the initialize request been sent? + SDL_Thread *communication_thread; + SDL_sem *quit_sem; + char *received_data; // dynamic array + SDL_mutex *error_mutex; + char error[256]; +} LSP; + +// @TODO: function declarations + +// 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); +void lsp_send_request(LSP *lsp, const LSPRequest *request); +const char *lsp_response_string(const LSPResponse *response, LSPString string); +bool lsp_create(LSP *lsp, const char *analyzer_command); +bool lsp_next_message(LSP *lsp, LSPMessage *message); +void lsp_free(LSP *lsp); @@ -1,4 +1,8 @@ /* +@TODO: +- rust-analyzer bug reports: + - bad json can give "Unexpected error: client exited without proper shutdown sequence" + - rust-analyzer should wait until cargo metadata/check is done before sending initialize response FUTURE FEATURES: - configurable max buffer size - better undo chaining (dechain on backspace?) @@ -92,6 +96,7 @@ static void die(char const *fmt, ...) { #include "ted.h" #include "gl.c" #include "text.c" +#include "lsp.h" #include "string32.c" #include "syntax.c" @@ -288,10 +293,11 @@ int main(int argc, char **argv) { PROFILE_TIME(init_start) PROFILE_TIME(basic_init_start) + + if (0) { // @TODO TEMPORARY - { LSP lsp={0}; - chdir("/p/test-lsp"); +// chdir("/p/test-lsp"); if (!lsp_create(&lsp, "rust-analyzer")) { printf("lsp_create: %s\n",lsp.error); exit(1); @@ -408,6 +414,14 @@ int main(int argc, char **argv) { } ted->last_save_time = -1e50; + + // @TODO TEMPORARY + ted->test_lsp = calloc(1, sizeof(LSP)); + if (!lsp_create(ted->test_lsp, "rust-analyzer")) { + printf("lsp_create: %s\n",ted->test_lsp->error); + exit(1); + } + // make sure signal handler has access to ted. error_signal_handler_ted = ted; @@ -66,6 +66,11 @@ Settings *ted_active_settings(Ted *ted) { return settings; } +LSP *ted_get_lsp(Ted *ted, Language lang) { + // @TODO + return ted->test_lsp; +} + u32 ted_color(Ted *ted, ColorSetting color) { return ted_active_settings(ted)->colors[color]; } @@ -353,6 +353,8 @@ typedef struct { } BuildError; typedef struct Ted { + struct LSP *test_lsp; // @TODO: something better + SDL_Window *window; Font *font_bold; Font *font; @@ -471,6 +473,7 @@ void ted_switch_to_buffer(Ted *ted, TextBuffer *buffer); // the settings of the active buffer, or the default settings if there is no active buffer Settings *ted_active_settings(Ted *ted); void ted_load_configs(Ted *ted, bool reloading); +struct LSP *ted_get_lsp(Ted *ted, Language lang); static TextBuffer *find_search_buffer(Ted *ted); // first, we read all config files, then we parse them. // this is because we want less specific settings (e.g. settings applied @@ -13,7 +13,7 @@ static bool unicode_is_start_of_code_point(u8 byte) { // *c will be filled with the next UTF-8 code point in `str`. `bytes` refers to the maximum // number of bytes that can be read from `str`. // Returns: -// 0 - if a NULL character was encountered +// 0 - if a null character was encountered // (size_t)-1 - on invalid UTF-8 // (size_t)-2 - on incomplete code point (str should be longer) // other - the number of bytes read from `str`. |