// This is an open source non-commercial project. Dear PVS-Studio, please check // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com /* * misc1.c: functions that didn't seem to fit elsewhere */ #include #include #include #include #include #include "nvim/ascii.h" #include "nvim/buffer.h" #include "nvim/buffer_updates.h" #include "nvim/charset.h" #include "nvim/cursor.h" #include "nvim/diff.h" #include "nvim/edit.h" #include "nvim/eval.h" #include "nvim/event/stream.h" #include "nvim/ex_cmds.h" #include "nvim/ex_docmd.h" #include "nvim/ex_getln.h" #include "nvim/fileio.h" #include "nvim/fold.h" #include "nvim/func_attr.h" #include "nvim/garray.h" #include "nvim/getchar.h" #include "nvim/indent.h" #include "nvim/indent_c.h" #include "nvim/main.h" #include "nvim/mbyte.h" #include "nvim/memline.h" #include "nvim/memory.h" #include "nvim/message.h" #include "nvim/misc1.h" #include "nvim/mouse.h" #include "nvim/move.h" #include "nvim/option.h" #include "nvim/os/input.h" #include "nvim/os/os.h" #include "nvim/os/shell.h" #include "nvim/os/signal.h" #include "nvim/os/time.h" #include "nvim/os_unix.h" #include "nvim/quickfix.h" #include "nvim/regexp.h" #include "nvim/screen.h" #include "nvim/search.h" #include "nvim/state.h" #include "nvim/strings.h" #include "nvim/tag.h" #include "nvim/ui.h" #include "nvim/undo.h" #include "nvim/vim.h" #include "nvim/window.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "misc1.c.generated.h" #endif // All user names (for ~user completion as done by shell). static garray_T ga_users = GA_EMPTY_INIT_VALUE; /* * get_leader_len() returns the length in bytes of the prefix of the given * string which introduces a comment. If this string is not a comment then * 0 is returned. * When "flags" is not NULL, it is set to point to the flags of the recognized * comment leader. * "backward" must be true for the "O" command. * If "include_space" is set, include trailing whitespace while calculating the * length. */ int get_leader_len(char_u *line, char_u **flags, bool backward, bool include_space) { int i, j; int result; int got_com = FALSE; int found_one; char_u part_buf[COM_MAX_LEN]; // buffer for one option part char_u *string; // pointer to comment string char_u *list; int middle_match_len = 0; char_u *prev_list; char_u *saved_flags = NULL; result = i = 0; while (ascii_iswhite(line[i])) { // leading white space is ignored ++i; } /* * Repeat to match several nested comment strings. */ while (line[i] != NUL) { /* * scan through the 'comments' option for a match */ found_one = FALSE; for (list = curbuf->b_p_com; *list; ) { /* Get one option part into part_buf[]. Advance "list" to next * one. Put "string" at start of string. */ if (!got_com && flags != NULL) { *flags = list; // remember where flags started } prev_list = list; (void)copy_option_part(&list, part_buf, COM_MAX_LEN, ","); string = vim_strchr(part_buf, ':'); if (string == NULL) { // missing ':', ignore this part continue; } *string++ = NUL; // isolate flags from string /* If we found a middle match previously, use that match when this * is not a middle or end. */ if (middle_match_len != 0 && vim_strchr(part_buf, COM_MIDDLE) == NULL && vim_strchr(part_buf, COM_END) == NULL) { break; } /* When we already found a nested comment, only accept further * nested comments. */ if (got_com && vim_strchr(part_buf, COM_NEST) == NULL) { continue; } // When 'O' flag present and using "O" command skip this one. if (backward && vim_strchr(part_buf, COM_NOBACK) != NULL) { continue; } /* Line contents and string must match. * When string starts with white space, must have some white space * (but the amount does not need to match, there might be a mix of * TABs and spaces). */ if (ascii_iswhite(string[0])) { if (i == 0 || !ascii_iswhite(line[i - 1])) { continue; // missing white space } while (ascii_iswhite(string[0])) { ++string; } } for (j = 0; string[j] != NUL && string[j] == line[i + j]; ++j) { ; } if (string[j] != NUL) { continue; // string doesn't match } /* When 'b' flag used, there must be white space or an * end-of-line after the string in the line. */ if (vim_strchr(part_buf, COM_BLANK) != NULL && !ascii_iswhite(line[i + j]) && line[i + j] != NUL) { continue; } /* We have found a match, stop searching unless this is a middle * comment. The middle comment can be a substring of the end * comment in which case it's better to return the length of the * end comment and its flags. Thus we keep searching with middle * and end matches and use an end match if it matches better. */ if (vim_strchr(part_buf, COM_MIDDLE) != NULL) { if (middle_match_len == 0) { middle_match_len = j; saved_flags = prev_list; } continue; } if (middle_match_len != 0 && j > middle_match_len) { /* Use this match instead of the middle match, since it's a * longer thus better match. */ middle_match_len = 0; } if (middle_match_len == 0) { i += j; } found_one = TRUE; break; } if (middle_match_len != 0) { /* Use the previously found middle match after failing to find a * match with an end. */ if (!got_com && flags != NULL) { *flags = saved_flags; } i += middle_match_len; found_one = TRUE; } // No match found, stop scanning. if (!found_one) { break; } result = i; // Include any trailing white space. while (ascii_iswhite(line[i])) { ++i; } if (include_space) { result = i; } // If this comment doesn't nest, stop here. got_com = TRUE; if (vim_strchr(part_buf, COM_NEST) == NULL) { break; } } return result; } /* * Return the offset at which the last comment in line starts. If there is no * comment in the whole line, -1 is returned. * * When "flags" is not null, it is set to point to the flags describing the * recognized comment leader. */ int get_last_leader_offset(char_u *line, char_u **flags) { int result = -1; int i, j; int lower_check_bound = 0; char_u *string; char_u *com_leader; char_u *com_flags; char_u *list; int found_one; char_u part_buf[COM_MAX_LEN]; // buffer for one option part /* * Repeat to match several nested comment strings. */ i = (int)STRLEN(line); while (--i >= lower_check_bound) { /* * scan through the 'comments' option for a match */ found_one = FALSE; for (list = curbuf->b_p_com; *list; ) { char_u *flags_save = list; /* * Get one option part into part_buf[]. Advance list to next one. * put string at start of string. */ (void)copy_option_part(&list, part_buf, COM_MAX_LEN, ","); string = vim_strchr(part_buf, ':'); if (string == NULL) { /* If everything is fine, this cannot actually * happen. */ continue; } *string++ = NUL; // Isolate flags from string. com_leader = string; /* * Line contents and string must match. * When string starts with white space, must have some white space * (but the amount does not need to match, there might be a mix of * TABs and spaces). */ if (ascii_iswhite(string[0])) { if (i == 0 || !ascii_iswhite(line[i - 1])) { continue; } while (ascii_iswhite(*string)) { string++; } } for (j = 0; string[j] != NUL && string[j] == line[i + j]; ++j) { /* do nothing */; } if (string[j] != NUL) { continue; } /* * When 'b' flag used, there must be white space or an * end-of-line after the string in the line. */ if (vim_strchr(part_buf, COM_BLANK) != NULL && !ascii_iswhite(line[i + j]) && line[i + j] != NUL) { continue; } if (vim_strchr(part_buf, COM_MIDDLE) != NULL) { // For a middlepart comment, only consider it to match if // everything before the current position in the line is // whitespace. Otherwise we would think we are inside a // comment if the middle part appears somewhere in the middle // of the line. E.g. for C the "*" appears often. for (j = 0; j <= i && ascii_iswhite(line[j]); j++) { } if (j < i) { continue; } } /* * We have found a match, stop searching. */ found_one = TRUE; if (flags) { *flags = flags_save; } com_flags = flags_save; break; } if (found_one) { char_u part_buf2[COM_MAX_LEN]; // buffer for one option part int len1, len2, off; result = i; /* * If this comment nests, continue searching. */ if (vim_strchr(part_buf, COM_NEST) != NULL) { continue; } lower_check_bound = i; /* Let's verify whether the comment leader found is a substring * of other comment leaders. If it is, let's adjust the * lower_check_bound so that we make sure that we have determined * the comment leader correctly. */ while (ascii_iswhite(*com_leader)) { ++com_leader; } len1 = (int)STRLEN(com_leader); for (list = curbuf->b_p_com; *list; ) { char_u *flags_save = list; (void)copy_option_part(&list, part_buf2, COM_MAX_LEN, ","); if (flags_save == com_flags) { continue; } string = vim_strchr(part_buf2, ':'); ++string; while (ascii_iswhite(*string)) { ++string; } len2 = (int)STRLEN(string); if (len2 == 0) { continue; } /* Now we have to verify whether string ends with a substring * beginning the com_leader. */ for (off = (len2 > i ? i : len2); off > 0 && off + len1 > len2; ) { --off; if (!STRNCMP(string + off, com_leader, len2 - off)) { if (i - off < lower_check_bound) { lower_check_bound = i - off; } } } } } } return result; } int gchar_pos(pos_T *pos) FUNC_ATTR_NONNULL_ARG(1) { // When searching columns is sometimes put at the end of a line. if (pos->col == MAXCOL) { return NUL; } return utf_ptr2char(ml_get_pos(pos)); } /* * check_status: called when the status bars for the buffer 'buf' * need to be updated */ void check_status(buf_T *buf) { FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { if (wp->w_buffer == buf && wp->w_status_height) { wp->w_redr_status = TRUE; if (must_redraw < VALID) { must_redraw = VALID; } } } } /// Ask for a reply from the user, 'y' or 'n' /// /// No other characters are accepted, the message is repeated until a valid /// reply is entered or is hit. /// /// @param[in] str Prompt: question to ask user. Is always followed by /// " (y/n)?". /// @param[in] direct Determines what function to use to get user input. If /// true then ui_inchar() will be used, otherwise vgetc(). /// I.e. when direct is true then characters are obtained /// directly from the user without buffers involved. /// /// @return 'y' or 'n'. Last is also what will be returned in case of interrupt. int ask_yesno(const char *const str, const bool direct) { const int save_State = State; no_wait_return++; State = CONFIRM; // Mouse behaves like with :confirm. setmouse(); // Disable mouse in xterm. no_mapping++; int r = ' '; while (r != 'y' && r != 'n') { // Same highlighting as for wait_return. smsg_attr(HL_ATTR(HLF_R), "%s (y/n)?", str); if (direct) { r = get_keystroke(NULL); } else { r = plain_vgetc(); } if (r == Ctrl_C || r == ESC) { r = 'n'; } msg_putchar(r); // Show what you typed. ui_flush(); } no_wait_return--; State = save_State; setmouse(); no_mapping--; return r; } /* * Return TRUE if "c" is a mouse key. */ int is_mouse_key(int c) { return c == K_LEFTMOUSE || c == K_LEFTMOUSE_NM || c == K_LEFTDRAG || c == K_LEFTRELEASE || c == K_LEFTRELEASE_NM || c == K_MOUSEMOVE || c == K_MIDDLEMOUSE || c == K_MIDDLEDRAG || c == K_MIDDLERELEASE || c == K_RIGHTMOUSE || c == K_RIGHTDRAG || c == K_RIGHTRELEASE || c == K_MOUSEDOWN || c == K_MOUSEUP || c == K_MOUSELEFT || c == K_MOUSERIGHT || c == K_X1MOUSE || c == K_X1DRAG || c == K_X1RELEASE || c == K_X2MOUSE || c == K_X2DRAG || c == K_X2RELEASE; } /* * Get a key stroke directly from the user. * Ignores mouse clicks and scrollbar events, except a click for the left * button (used at the more prompt). * Doesn't use vgetc(), because it syncs undo and eats mapped characters. * Disadvantage: typeahead is ignored. * Translates the interrupt character for unix to ESC. */ int get_keystroke(MultiQueue *events) { char_u *buf = NULL; int buflen = 150; int maxlen; int len = 0; int n; int save_mapped_ctrl_c = mapped_ctrl_c; int waited = 0; mapped_ctrl_c = 0; // mappings are not used here for (;; ) { // flush output before waiting ui_flush(); /* Leave some room for check_termcode() to insert a key code into (max * 5 chars plus NUL). And fix_input_buffer() can triple the number of * bytes. */ maxlen = (buflen - 6 - len) / 3; if (buf == NULL) { buf = xmalloc((size_t)buflen); } else if (maxlen < 10) { // Need some more space. This might happen when receiving a long // escape sequence. buflen += 100; buf = xrealloc(buf, (size_t)buflen); maxlen = (buflen - 6 - len) / 3; } /* First time: blocking wait. Second time: wait up to 100ms for a * terminal code to complete. */ n = os_inchar(buf + len, maxlen, len == 0 ? -1L : 100L, 0, events); if (n > 0) { // Replace zero and CSI by a special key code. n = fix_input_buffer(buf + len, n); len += n; waited = 0; } else if (len > 0) { ++waited; // keep track of the waiting time } if (n > 0) { // found a termcode: adjust length len = n; } if (len == 0) { // nothing typed yet continue; } // Handle modifier and/or special key code. n = buf[0]; if (n == K_SPECIAL) { n = TO_SPECIAL(buf[1], buf[2]); if (buf[1] == KS_MODIFIER || n == K_IGNORE || (is_mouse_key(n) && n != K_LEFTMOUSE)) { if (buf[1] == KS_MODIFIER) { mod_mask = buf[2]; } len -= 3; if (len > 0) { memmove(buf, buf + 3, (size_t)len); } continue; } break; } if (MB_BYTE2LEN(n) > len) { // more bytes to get. continue; } buf[len >= buflen ? buflen - 1 : len] = NUL; n = utf_ptr2char(buf); break; } xfree(buf); mapped_ctrl_c = save_mapped_ctrl_c; return n; } /// Get a number from the user. /// When "mouse_used" is not NULL allow using the mouse. /// /// @param colon allow colon to abort int get_number(int colon, int *mouse_used) { int n = 0; int c; int typed = 0; if (mouse_used != NULL) { *mouse_used = FALSE; } /* When not printing messages, the user won't know what to type, return a * zero (as if CR was hit). */ if (msg_silent != 0) { return 0; } no_mapping++; for (;; ) { ui_cursor_goto(msg_row, msg_col); c = safe_vgetc(); if (ascii_isdigit(c)) { n = n * 10 + c - '0'; msg_putchar(c); ++typed; } else if (c == K_DEL || c == K_KDEL || c == K_BS || c == Ctrl_H) { if (typed > 0) { MSG_PUTS("\b \b"); --typed; } n /= 10; } else if (mouse_used != NULL && c == K_LEFTMOUSE) { *mouse_used = TRUE; n = mouse_row + 1; break; } else if (n == 0 && c == ':' && colon) { stuffcharReadbuff(':'); if (!exmode_active) { cmdline_row = msg_row; } skip_redraw = true; // skip redraw once do_redraw = false; break; } else if (c == Ctrl_C || c == ESC || c == 'q') { n = 0; break; } else if (c == CAR || c == NL) { break; } } no_mapping--; return n; } /* * Ask the user to enter a number. * When "mouse_used" is not NULL allow using the mouse and in that case return * the line number. */ int prompt_for_number(int *mouse_used) { int i; int save_cmdline_row; int save_State; // When using ":silent" assume that was entered. if (mouse_used != NULL) { MSG_PUTS(_("Type number and or click with the mouse " "(q or empty cancels): ")); } else { MSG_PUTS(_("Type number and (q or empty cancels): ")); } /* Set the state such that text can be selected/copied/pasted and we still * get mouse events. */ save_cmdline_row = cmdline_row; cmdline_row = 0; save_State = State; State = ASKMORE; // prevents a screen update when using a timer // May show different mouse shape. setmouse(); i = get_number(TRUE, mouse_used); if (KeyTyped) { // don't call wait_return() now if (msg_row > 0) { cmdline_row = msg_row - 1; } need_wait_return = false; msg_didany = false; msg_didout = false; } else { cmdline_row = save_cmdline_row; } State = save_State; // May need to restore mouse shape. setmouse(); return i; } void msgmore(long n) { long pn; if (global_busy // no messages now, wait until global is finished || !messaging()) { // 'lazyredraw' set, don't do messages now return; } /* We don't want to overwrite another important message, but do overwrite * a previous "more lines" or "fewer lines" message, so that "5dd" and * then "put" reports the last action. */ if (keep_msg != NULL && !keep_msg_more) { return; } if (n > 0) { pn = n; } else { pn = -n; } if (pn > p_report) { if (pn == 1) { if (n > 0) { STRLCPY(msg_buf, _("1 more line"), MSG_BUF_LEN); } else { STRLCPY(msg_buf, _("1 line less"), MSG_BUF_LEN); } } else { if (n > 0) { vim_snprintf((char *)msg_buf, MSG_BUF_LEN, _("%" PRId64 " more lines"), (int64_t)pn); } else { vim_snprintf((char *)msg_buf, MSG_BUF_LEN, _("%" PRId64 " fewer lines"), (int64_t)pn); } } if (got_int) { xstrlcat((char *)msg_buf, _(" (Interrupted)"), MSG_BUF_LEN); } if (msg(msg_buf)) { set_keep_msg(msg_buf, 0); keep_msg_more = true; } } } /* * flush map and typeahead buffers and give a warning for an error */ void beep_flush(void) { if (emsg_silent == 0) { flush_buffers(FLUSH_MINIMAL); vim_beep(BO_ERROR); } } // Give a warning for an error // val is one of the BO_ values, e.g., BO_OPER void vim_beep(unsigned val) { called_vim_beep = true; if (emsg_silent == 0) { if (!((bo_flags & val) || (bo_flags & BO_ALL))) { static int beeps = 0; static uint64_t start_time = 0; // Only beep up to three times per half a second, // otherwise a sequence of beeps would freeze Vim. if (start_time == 0 || os_hrtime() - start_time > 500000000u) { beeps = 0; start_time = os_hrtime(); } beeps++; if (beeps <= 3) { if (p_vb) { ui_call_visual_bell(); } else { ui_call_bell(); } } } // When 'debug' contains "beep" produce a message. If we are sourcing // a script or executing a function give the user a hint where the beep // comes from. if (vim_strchr(p_debug, 'e') != NULL) { msg_source(HL_ATTR(HLF_W)); msg_attr(_("Beep!"), HL_ATTR(HLF_W)); } } } #if defined(EXITFREE) void free_users(void) { ga_clear_strings(&ga_users); } #endif /* * Find all user names for user completion. * Done only once and then cached. */ static void init_users(void) { static int lazy_init_done = FALSE; if (lazy_init_done) { return; } lazy_init_done = TRUE; os_get_usernames(&ga_users); } /* * Function given to ExpandGeneric() to obtain an user names. */ char_u *get_users(expand_T *xp, int idx) { init_users(); if (idx < ga_users.ga_len) { return ((char_u **)ga_users.ga_data)[idx]; } return NULL; } /* * Check whether name matches a user name. Return: * 0 if name does not match any user name. * 1 if name partially matches the beginning of a user name. * 2 is name fully matches a user name. */ int match_user(char_u *name) { int n = (int)STRLEN(name); int result = 0; init_users(); for (int i = 0; i < ga_users.ga_len; i++) { if (STRCMP(((char_u **)ga_users.ga_data)[i], name) == 0) { return 2; // full match } if (STRNCMP(((char_u **)ga_users.ga_data)[i], name, n) == 0) { result = 1; // partial match } } return result; } /// Preserve files and exit. /// @note IObuff must contain a message. /// @note This may be called from deadly_signal() in a signal handler, avoid /// unsafe functions, such as allocating memory. void preserve_exit(void) FUNC_ATTR_NORETURN { // 'true' when we are sure to exit, e.g., after a deadly signal static bool really_exiting = false; // Prevent repeated calls into this method. if (really_exiting) { if (input_global_fd() >= 0) { // normalize stream (#2598) stream_set_blocking(input_global_fd(), true); } exit(2); } really_exiting = true; // Ignore SIGHUP while we are already exiting. #9274 signal_reject_deadly(); mch_errmsg(IObuff); mch_errmsg("\n"); ui_flush(); ml_close_notmod(); // close all not-modified buffers FOR_ALL_BUFFERS(buf) { if (buf->b_ml.ml_mfp != NULL && buf->b_ml.ml_mfp->mf_fname != NULL) { mch_errmsg("Vim: preserving files...\r\n"); ui_flush(); ml_sync_all(false, false, true); // preserve all swap files break; } } ml_close_all(false); // close all memfiles, without deleting mch_errmsg("Vim: Finished.\r\n"); getout(1); } /* * Check for CTRL-C pressed, but only once in a while. * Should be used instead of os_breakcheck() for functions that check for * each line in the file. Calling os_breakcheck() each time takes too much * time, because it can be a system call. */ #ifndef BREAKCHECK_SKIP # define BREAKCHECK_SKIP 1000 #endif static int breakcheck_count = 0; void line_breakcheck(void) { if (++breakcheck_count >= BREAKCHECK_SKIP) { breakcheck_count = 0; os_breakcheck(); } } /* * Like line_breakcheck() but check 10 times less often. */ void fast_breakcheck(void) { if (++breakcheck_count >= BREAKCHECK_SKIP * 10) { breakcheck_count = 0; os_breakcheck(); } } // Like line_breakcheck() but check 100 times less often. void veryfast_breakcheck(void) { if (++breakcheck_count >= BREAKCHECK_SKIP * 100) { breakcheck_count = 0; os_breakcheck(); } } /// os_call_shell() wrapper. Handles 'verbose', :profile, and v:shell_error. /// Invalidates cached tags. /// /// @return shell command exit code int call_shell(char_u *cmd, ShellOpts opts, char_u *extra_shell_arg) { int retval; proftime_T wait_time; if (p_verbose > 3) { verbose_enter(); smsg(_("Executing command: \"%s\""), cmd == NULL ? p_sh : cmd); msg_putchar('\n'); verbose_leave(); } if (do_profiling == PROF_YES) { prof_child_enter(&wait_time); } if (*p_sh == NUL) { EMSG(_(e_shellempty)); retval = -1; } else { // The external command may update a tags file, clear cached tags. tag_freematch(); retval = os_call_shell(cmd, opts, extra_shell_arg); } set_vim_var_nr(VV_SHELL_ERROR, (varnumber_T)retval); if (do_profiling == PROF_YES) { prof_child_exit(&wait_time); } return retval; } /// Get the stdout of an external command. /// If "ret_len" is NULL replace NUL characters with NL. When "ret_len" is not /// NULL store the length there. /// /// @param cmd command to execute /// @param infile optional input file name /// @param flags can be kShellOptSilent or 0 /// @param ret_len length of the stdout /// /// @return an allocated string, or NULL for error. char_u *get_cmd_output(char_u *cmd, char_u *infile, ShellOpts flags, size_t *ret_len) { char_u *buffer = NULL; if (check_secure()) { return NULL; } // get a name for the temp file char_u *tempname = vim_tempname(); if (tempname == NULL) { EMSG(_(e_notmp)); return NULL; } // Add the redirection stuff char_u *command = make_filter_cmd(cmd, infile, tempname); /* * Call the shell to execute the command (errors are ignored). * Don't check timestamps here. */ ++no_check_timestamps; call_shell(command, kShellOptDoOut | kShellOptExpand | flags, NULL); --no_check_timestamps; xfree(command); // read the names from the file into memory FILE *fd = os_fopen((char *)tempname, READBIN); if (fd == NULL) { EMSG2(_(e_notopen), tempname); goto done; } fseek(fd, 0L, SEEK_END); size_t len = (size_t)ftell(fd); // get size of temp file fseek(fd, 0L, SEEK_SET); buffer = xmalloc(len + 1); size_t i = fread((char *)buffer, 1, len, fd); fclose(fd); os_remove((char *)tempname); if (i != len) { EMSG2(_(e_notread), tempname); XFREE_CLEAR(buffer); } else if (ret_len == NULL) { // Change NUL into SOH, otherwise the string is truncated. for (i = 0; i < len; ++i) { if (buffer[i] == NUL) { buffer[i] = 1; } } buffer[len] = NUL; // make sure the buffer is terminated } else { *ret_len = len; } done: xfree(tempname); return buffer; } /* * Free the list of files returned by expand_wildcards() or other expansion * functions. */ void FreeWild(int count, char_u **files) { if (count <= 0 || files == NULL) { return; } while (count--) { xfree(files[count]); } xfree(files); } /* * Return TRUE when need to go to Insert mode because of 'insertmode'. * Don't do this when still processing a command or a mapping. * Don't do this when inside a ":normal" command. */ int goto_im(void) { return p_im && stuff_empty() && typebuf_typed(); } /// Put the timestamp of an undo header in "buf[buflen]" in a nice format. void add_time(char_u *buf, size_t buflen, time_t tt) { struct tm curtime; if (time(NULL) - tt >= 100) { os_localtime_r(&tt, &curtime); if (time(NULL) - tt < (60L * 60L * 12L)) { // within 12 hours (void)strftime((char *)buf, buflen, "%H:%M:%S", &curtime); } else { // longer ago (void)strftime((char *)buf, buflen, "%Y/%m/%d %H:%M:%S", &curtime); } } else { int64_t seconds = time(NULL) - tt; vim_snprintf((char *)buf, buflen, NGETTEXT("%" PRId64 " second ago", "%" PRId64 " seconds ago", (uint32_t)seconds), seconds); } }