diff --git a/src/gen/gen_options.lua b/src/gen/gen_options.lua index cc5de9fad2..0ee9f3d67a 100644 --- a/src/gen/gen_options.lua +++ b/src/gen/gen_options.lua @@ -684,7 +684,7 @@ local function gen_keysets(output_file) write('') write(('extern const OptSchemaItem opt_%s_schema[];'):format(abbr)) write('') - write(('/// %s'):format(o.full_name)) + write(("/// '%s'"):format(o.full_name)) write(('typedef struct %s {'):format(kd)) write((' OptionalKeys is_set__%s_;'):format(abbr)) for _, item in ipairs(o.schema.dict) do diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 38646bd0e2..446e31b4ed 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -1208,10 +1208,11 @@ struct window_S { pos_save_T w_save_cursor; // backup of cursor pos and topline bool w_do_win_fix_cursor; // if true cursor may be invalid - int w_maxwidth; // maxwidth of 'previewpopup' window - int w_maxheight; // maxheight of 'previewpopup' window - int w_wantline; // "line" for 'previewpopup' window - int w_wantcol; // "col" for 'previewpopup' window + // Screen pos 'previewpopup' anchors to (original cursor pos). Separate from WinConfig because + // win_float_update_preview() re-autosizes as content updates, and re-decides the flip + // above/below. WinConfig.row/col hold the placed (offset, flipped, clamped) result. + int w_wantline; + int w_wantcol; int w_winrow_off; ///< offset from winrow to the inner window area int w_wincol_off; ///< offset from wincol to the inner window area diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index cef02bb0a2..17455c5cdd 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -4812,10 +4812,10 @@ void free_old_sub(void) /// Set up for a tagpreview. /// -/// @param undo_sync sync undo when leaving the window -/// @param use_previewpopup use floating window if 'previewpopup' set +/// @param undo_sync Sync undo when leaving the window. +/// @param use_previewpopup Use a floating window (when 'previewpopup' is set). /// -/// @return true when it was created. +/// @return true when the preview window was created. bool prepare_tagpreview(bool undo_sync, bool use_previewpopup) { if (curwin->w_p_pvw) { @@ -4828,11 +4828,8 @@ bool prepare_tagpreview(bool undo_sync, bool use_previewpopup) if (use_previewpopup ? wp->w_kind != kWinPreview : wp->w_floating) { continue; } - - // if find a floating preview window update wantline/wantcol if (use_previewpopup) { - wp->w_wantline = curwin->w_winrow + curwin->w_wrow; - wp->w_wantcol = curwin->w_wincol + curwin->w_wcol; + win_float_anchor_preview(wp); // Re-anchor to the new cursor position. } win_enter(wp, undo_sync); return false; diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index ed1670e1fe..8aaab95932 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -2100,12 +2100,9 @@ static bool parse_border_opt(char *border_opt) { WinConfig fconfig = WIN_CONFIG_INIT; Error err = ERROR_INIT; - bool result = true; - if (!parse_winborder(&fconfig, border_opt, &err)) { - result = false; - } + bool ok = parse_winborder(&fconfig, border_opt, &err); api_clear_error(&err); - return result; + return ok; } /// The 'winborder' option is changed. @@ -2721,7 +2718,7 @@ const char *check_chars_options(void) const char *did_set_previewpopup(optset_T *args) { WinConfig fconfig = WIN_CONFIG_INIT; - if (!win_previewpopup_config(p_pvp, &fconfig)) { + if (!win_previewpopup_config(&fconfig)) { return e_invarg; } return NULL; @@ -2730,14 +2727,15 @@ const char *did_set_previewpopup(optset_T *args) int expand_set_popupoption(optexpand_T *args, int *numMatches, char ***matches) { expand_T *xp = args->oe_xp; - if (xp->xp_pattern - args->oe_set_arg >= 7 && strncmp(xp->xp_pattern - 7, "border:", 7) == 0) { - return expand_set_opt_string(args, opt_pvp_border_values, ARRAY_SIZE(opt_pvp_border_values) - 1, - numMatches, matches); - } + if (xp->xp_pattern > args->oe_set_arg && *(xp->xp_pattern - 1) == ':') { - return FAIL; + if (completing_value_for_subopt(args, "border")) { + return expand_set_opt_string(args, opt_pvp_border_values, + ARRAY_SIZE(opt_pvp_border_values) - 1, numMatches, matches); + } + return FAIL; // "height:"/"width:" = number, nothing to complete. } - return expand_set_opt_string(args, opt_pvp_values, - ARRAY_SIZE(opt_pvp_values) - 1, + + return expand_set_opt_string(args, opt_pvp_values, ARRAY_SIZE(opt_pvp_values) - 1, numMatches, matches); } diff --git a/src/nvim/winfloat.c b/src/nvim/winfloat.c index 0fab93f61b..6992a7e71d 100644 --- a/src/nvim/winfloat.c +++ b/src/nvim/winfloat.c @@ -411,8 +411,8 @@ win_T *win_float_find_altwin(const win_T *win, const tabpage_T *tp) return (wp != win && wp->w_config.focusable && !wp->w_config.hide) ? wp : tp->tp_firstwin; } -/// Inline helper function for handling errors and cleanup in win_float_create_preview. -static inline win_T *handle_error_and_cleanup(win_T *wp, Error *err) +/// Abort win_float_special(): emit any error, tear down the half-built window, unblock autocmds. +static inline win_T *win_float_special_fail(win_T *wp, Error *err) { if (ERROR_SET(err)) { emsg(err->msg); @@ -426,13 +426,20 @@ static inline win_T *handle_error_and_cleanup(win_T *wp, Error *err) return NULL; } -/// create a floating preview window. +/// Snapshot a 'previewpopup' preview's anchor: the current cursor's screen cell. +/// Call before the preview win is "entered" (then it becomes `curwin`). +void win_float_anchor_preview(win_T *wp) + FUNC_ATTR_NONNULL_ALL +{ + wp->w_wantline = curwin->w_winrow + curwin->w_wrow; + wp->w_wantcol = curwin->w_wincol + curwin->w_wcol; +} + +/// Creates a special floatwin: a 'previewpopup' preview or a pum-info popup. /// -/// @param[in] bool enter floating window. -/// @param[in] bool create a new buffer for window. -/// @param[in] int floating preview window kind. -/// -/// @return win_T +/// @param enter Enter the new window. +/// @param new_buf Give the window a new scratch buffer. +/// @param kind kWinPreview or kWinInfo. win_T *win_float_special(bool enter, bool new_buf, WinKind kind) { WinConfig config = WIN_CONFIG_INIT; @@ -447,7 +454,7 @@ win_T *win_float_special(bool enter, bool new_buf, WinKind kind) config.style = kWinStyleMinimal; Error err = ERROR_INIT; bool preview = kind == kWinPreview; - if (preview && !win_previewpopup_config(p_pvp, &config)) { + if (preview && !win_previewpopup_config(&config)) { emsg(_(e_invarg)); return NULL; } @@ -455,24 +462,24 @@ win_T *win_float_special(bool enter, bool new_buf, WinKind kind) block_autocmds(); win_T *wp = win_new_float(NULL, false, config, &err); if (!wp) { - return handle_error_and_cleanup(wp, &err); + return win_float_special_fail(wp, &err); } if (new_buf) { Buffer b = nvim_create_buf(false, true, &err); if (!b) { - return handle_error_and_cleanup(wp, &err); + return win_float_special_fail(wp, &err); } buf_T *buf = find_buffer_by_handle(b, &err); if (!buf) { - return handle_error_and_cleanup(wp, &err); + return win_float_special_fail(wp, &err); } buf->b_p_bl = false; // unlist set_option_direct_for(kOptBufhidden, STATIC_CSTR_AS_OPTVAL("wipe"), OPT_LOCAL, 0, kOptScopeBuf, buf); win_set_buf(wp, buf, &err); if (ERROR_SET(&err)) { - return handle_error_and_cleanup(wp, &err); + return win_float_special_fail(wp, &err); } } unblock_autocmds(); @@ -483,10 +490,7 @@ win_T *win_float_special(bool enter, bool new_buf, WinKind kind) if (preview) { wp->w_p_pvw = true; RESET_BINDING(wp); - wp->w_maxheight = config.height; - wp->w_maxwidth = config.width; - wp->w_wantline = curwin->w_winrow + curwin->w_wrow; - wp->w_wantcol = curwin->w_wincol + curwin->w_wcol; + win_float_anchor_preview(wp); } wp->w_kind = kind; @@ -508,11 +512,8 @@ void win_float_close(WinKind kind) } } -/// Update the preview window from its content: the title (the previewed file's -/// name), the size ('previewpopup' dimensions or autosized to the content), and -/// the position. -/// -/// @param wp pointer of floating preview window +/// Re-layout a 'previewpopup' window for its current content: title, size, and anchored position. +/// No-op if `wp` is not a floating 'previewwindow'. void win_float_update_preview(win_T *wp) FUNC_ATTR_NONNULL_ALL { @@ -538,15 +539,17 @@ void win_float_update_preview(win_T *wp) int row = MIN(MAX(wp->w_wantline, 0), Rows - 1); int col = MIN(MAX(wp->w_wantcol, 0), Columns - 1); + OptKeyDict_pvp *v = opt_keyset(p_pvp, kOptPreviewpopup, NULL); + int want_w = HAS_KEY(v, pvp, width) ? (int)v->width : 0; + int want_h = HAS_KEY(v, pvp, height) ? (int)v->height : 0; + // If a dimension is not given in 'previewpopup', autosize to the content. linenr_T last = wp->w_buffer->b_ml.ml_line_count; - int want_w = wp->w_maxwidth; if (want_w == 0) { want_w = MAX(win_max_displaywidth(wp, 1, last, Columns), 1); } want_w = MIN(want_w, Columns); - int want_h = wp->w_maxheight; if (want_h == 0) { int saved_view_width = wp->w_view_width; wp->w_view_width = want_w; @@ -576,44 +579,38 @@ void win_float_update_preview(win_T *wp) win_config_float(wp, wp->w_config); } -/// Parse 'previewpopup' ("height:N,width:N,border:style") into `config`. -/// -/// @param value the option value to parse (e.g. 'previewpopup') +/// Applies 'previewpopup' ("height:N,width:N,border:style") to `config`. /// /// @return false on an invalid value; caller emits E474. -bool win_previewpopup_config(char *value, WinConfig *config) +bool win_previewpopup_config(WinConfig *config) FUNC_ATTR_NONNULL_ALL { - // Basic validation was done by opt_strings_check; do more validation here. - OptKeyDict_pvp *v = opt_keyset(value, kOptPreviewpopup, NULL); - bool ok = true; + OptKeyDict_pvp *v = opt_keyset(p_pvp, kOptPreviewpopup, NULL); if ((HAS_KEY(v, pvp, height) && v->height < 1) || (HAS_KEY(v, pvp, width) && v->width < 1)) { - ok = false; + return false; } - - bool has_border = HAS_KEY(v, pvp, border); - if (ok && has_border) { - Error err = ERROR_INIT; - ok = parse_winborder(config, v->border, &err); - api_clear_error(&err); - } - config->height = HAS_KEY(v, pvp, height) ? (int)v->height : 0; config->width = HAS_KEY(v, pvp, width) ? (int)v->width : 0; - if (!has_border) { - config->border = false; - if (*p_winborder != NUL) { - Error err = ERROR_INIT; - parse_winborder(config, p_winborder, &err); - api_clear_error(&err); + // Use 'previewpopup' border if set, else fallback to 'winborder'. + char *border = NULL; + if (HAS_KEY(v, pvp, border)) { + border = v->border; + } else if (*p_winborder != NUL) { + border = p_winborder; + } + if (border != NULL) { + Error err = ERROR_INIT; + bool ok = parse_winborder(config, border, &err); + api_clear_error(&err); + if (!ok) { + return false; } } if (!config->border) { config->title = false; } - - return ok; + return true; } diff --git a/test/functional/ui/preview_spec.lua b/test/functional/ui/preview_spec.lua index 2474402bd5..843334525d 100644 --- a/test/functional/ui/preview_spec.lua +++ b/test/functional/ui/preview_spec.lua @@ -9,46 +9,33 @@ local clear, command, api, fn = n.clear, n.command, n.api, n.fn local eq, pcall_err, write_file = t.eq, t.pcall_err, t.write_file local exec, feed = n.exec, n.feed +--- Joins lines "1".."count", with `overrides` (1-based row → text) substituted. +--- @param count integer +--- @param overrides table +local function numbered_lines(count, overrides) + local lines = {} --- @type string[] + for i = 1, count do + lines[i] = overrides[i] or tostring(i) + end + return table.concat(lines, '\n') +end + describe("'previewpopup'", function() before_each(function() clear() end) local function with_ext_multigrid(multigrid) - local screen, attrs + local screen ---@type test.functional.ui.screen before_each(function() screen = Screen.new(40, 7, { ext_multigrid = multigrid }) - attrs = { - [0] = { bold = true, foreground = Screen.colors.Blue }, - [1] = { background = Screen.colors.LightMagenta }, - [2] = { - background = Screen.colors.LightMagenta, - bold = true, - foreground = Screen.colors.Blue1, - }, - [3] = { bold = true }, - [4] = { bold = true, reverse = true }, - [5] = { reverse = true }, - [6] = { background = Screen.colors.LightMagenta, bold = true, reverse = true }, - [7] = { foreground = Screen.colors.White, background = Screen.colors.Red }, - [8] = { bold = true, foreground = Screen.colors.SeaGreen4 }, - [9] = { background = Screen.colors.LightGrey, underline = true }, - [10] = { - background = Screen.colors.LightGrey, - underline = true, - bold = true, - foreground = Screen.colors.Magenta, - }, - [11] = { bold = true, foreground = Screen.colors.Magenta }, - [12] = { background = Screen.colors.WebGrey }, - [13] = { background = Screen.colors.Yellow }, - [14] = { + screen:add_extra_attr_ids({ + [100] = { foreground = Screen.colors.Magenta1, background = Screen.colors.Plum1, bold = true, }, - } - screen:set_default_attr_ids(attrs) + }) end) it('validation', function() @@ -84,7 +71,7 @@ describe("'previewpopup'", function() end) -- oldtest: Test_previewpopup - it('works with tags and search', function() + it('with tags and search', function() finally(function() os.remove('Xtags') os.remove('Xtagfile') @@ -99,29 +86,11 @@ describe("'previewpopup'", function() theword Xtagfile /^theword ]]) ) - local tagfile_lines = {} - for i = 1, 20 do - table.insert(tagfile_lines, tostring(i)) - end - table.insert(tagfile_lines, 'theword is here') - for i = 22, 27 do - table.insert(tagfile_lines, tostring(i)) - end - table.insert(tagfile_lines, 'this is another place') - for i = 29, 40 do - table.insert(tagfile_lines, tostring(i)) - end - write_file('Xtagfile', table.concat(tagfile_lines, '\n')) - - local header_lines = {} - for i = 1, 10 do - table.insert(header_lines, tostring(i)) - end - table.insert(header_lines, 'searched word is here') - for i = 12, 20 do - table.insert(header_lines, tostring(i)) - end - write_file('Xheader.h', table.concat(header_lines, '\n')) + write_file( + 'Xtagfile', + numbered_lines(40, { [21] = 'theword is here', [28] = 'this is another place' }) + ) + write_file('Xheader.h', numbered_lines(20, { [11] = 'searched word is here' })) command('set tags=Xtags') api.nvim_buf_set_lines(0, 0, -1, false, { 'one', @@ -155,21 +124,21 @@ describe("'previewpopup'", function() five | six | seven | - find {13:^theword} somewhere | + find {10:^theword} somewhere | nine | this is another word | very long line where the word is also an| other | - {0:~ }|*7 + {1:~ }|*7 ## grid 3 /theword | ## grid 4 - {1:┌────────────}{14:Xtagfile}{1:────────────┐}| - {1:│20 │}| - {1:│}{13:theword}{1: is here │}| - {1:│22 │}| - {1:│23 │}| - {1:└────────────────────────────────┘}| + {4:┌────────────}{100:Xtagfile}{4:────────────┐}| + {4:│20 │}| + {4:│}{10:theword}{4: is here │}| + {4:│22 │}| + {4:│23 │}| + {4:└────────────────────────────────┘}| ]], win_pos = { [2] = { height = 19, startcol = 0, startrow = 0, width = 40, win = 1000 }, @@ -187,14 +156,14 @@ describe("'previewpopup'", function() five | six | seven | - find {13:^theword} somewhere | - nine {1:┌────────────}{14:Xtagfile}{1:────────────┐}| - this i{1:│20 │}| - very l{1:│}{13:theword}{1: is here │}| - other {1:│22 │}| - {0:~ }{1:│23 │}| - {0:~ }{1:└────────────────────────────────┘}| - {0:~ }|*5 + find {10:^theword} somewhere | + nine {4:┌────────────}{100:Xtagfile}{4:────────────┐}| + this i{4:│20 │}| + very l{4:│}{10:theword}{4: is here │}| + other {4:│22 │}| + {1:~ }{4:│23 │}| + {1:~ }{4:└────────────────────────────────┘}| + {1:~ }|*5 /theword | ]]) end @@ -214,12 +183,12 @@ describe("'previewpopup'", function() five | six | seven | - find {13:^theword} somewhere | + find {10:^theword} somewhere | nine | this is another word | very long line where the word is also an| other | - {0:~ }|*7 + {1:~ }|*7 ## grid 3 /theword | ]], @@ -236,12 +205,12 @@ describe("'previewpopup'", function() five | six | seven | - find {13:^theword} somewhere | + find {10:^theword} somewhere | nine | this is another word | very long line where the word is also an| other | - {0:~ }|*7 + {1:~ }|*7 /theword | ]]) end @@ -262,21 +231,21 @@ describe("'previewpopup'", function() five | six | seven | - find {13:^theword} somewhere | + find {10:^theword} somewhere | nine | this is another word | very long line where the word is also an| other | - {0:~ }|*7 + {1:~ }|*7 ## grid 3 /theword | ## grid 5 - {1:┌────────────}{14:Xtagfile}{1:────────────┐}| - {1:│20 │}| - {1:│}{13:theword}{1: is here │}| - {1:│22 │}| - {1:│23 │}| - {1:└────────────────────────────────┘}| + {4:┌────────────}{100:Xtagfile}{4:────────────┐}| + {4:│20 │}| + {4:│}{10:theword}{4: is here │}| + {4:│22 │}| + {4:│23 │}| + {4:└────────────────────────────────┘}| ]], win_pos = { [2] = { height = 19, startcol = 0, startrow = 0, width = 40, win = 1000 }, @@ -294,14 +263,14 @@ describe("'previewpopup'", function() five | six | seven | - find {13:^theword} somewhere | - nine {1:┌────────────}{14:Xtagfile}{1:────────────┐}| - this i{1:│20 │}| - very l{1:│}{13:theword}{1: is here │}| - other {1:│22 │}| - {0:~ }{1:│23 │}| - {0:~ }{1:└────────────────────────────────┘}| - {0:~ }|*5 + find {10:^theword} somewhere | + nine {4:┌────────────}{100:Xtagfile}{4:────────────┐}| + this i{4:│20 │}| + very l{4:│}{10:theword}{4: is here │}| + other {4:│22 │}| + {1:~ }{4:│23 │}| + {1:~ }{4:└────────────────────────────────┘}| + {1:~ }|*5 /theword | ]]) end @@ -321,21 +290,21 @@ describe("'previewpopup'", function() five | six | seven | - find {13:^theword} somewhere | + find {10:^theword} somewhere | nine | this is another word | very long line where the word is also an| other | - {0:~ }|*7 + {1:~ }|*7 ## grid 3 /theword | ## grid 6 - {1:┌───────────}{14:Xheader.h}{1:────────────┐}| - {1:│10 │}| - {1:│searched word is here │}| - {1:│12 │}| - {1:│13 │}| - {1:└────────────────────────────────┘}| + {4:┌───────────}{100:Xheader.h}{4:────────────┐}| + {4:│10 │}| + {4:│searched word is here │}| + {4:│12 │}| + {4:│13 │}| + {4:└────────────────────────────────┘}| ]], win_pos = { [2] = { height = 19, startcol = 0, startrow = 0, width = 40, win = 1000 }, @@ -353,14 +322,14 @@ describe("'previewpopup'", function() five | six | seven | - find {13:^theword} somewhere | - nine {1:┌───────────}{14:Xheader.h}{1:────────────┐}| - this i{1:│10 │}| - very l{1:│searched word is here │}| - other {1:│12 │}| - {0:~ }{1:│13 │}| - {0:~ }{1:└────────────────────────────────┘}| - {0:~ }|*5 + find {10:^theword} somewhere | + nine {4:┌───────────}{100:Xheader.h}{4:────────────┐}| + this i{4:│10 │}| + very l{4:│searched word is here │}| + other {4:│12 │}| + {1:~ }{4:│13 │}| + {1:~ }{4:└────────────────────────────────┘}| + {1:~ }|*5 /theword | ]]) end @@ -385,14 +354,14 @@ describe("'previewpopup'", function() end) local expect_screen = [[ one other^ | - t{1:le}{12: other }{1: }| - t{1:le once }| - o{1:ec only }| - o{1:ec off }| - o{1:ca one hello') }| - o{1:" the end }| - {0:~ }|*2 - {3:-- }{8:match 1 of 5} | + t{4:le}{12: other }{4: }| + t{4:le once }| + o{4:ec only }| + o{4:ec off }| + o{4:ca one hello') }| + o{4:" the end }| + {1:~ }|*2 + {5:-- }{6:match 1 of 5} | ]] -- oldtest: Test_previewpopup_pum_pedit @@ -425,7 +394,7 @@ describe("'previewpopup'", function() end) end) - it('pedit and border overrides', function() + it(':pedit and border overrides', function() command('call writefile(["bar"], "foo", "a")') finally(function() os.remove('foo') @@ -439,14 +408,14 @@ describe("'previewpopup'", function() [3:----------------------------------------]| ## grid 2 ^ | - {0:~ }|*5 + {1:~ }|*5 ## grid 3 | ## grid 4 - {1:┌─}{14:foo}{1:─┐}| - {1:│bar │}| - {1:│ │}| - {1:└─────┘}| + {4:┌─}{100:foo}{4:─┐}| + {4:│bar │}| + {4:│ │}| + {4:└─────┘}| ]], win_pos = { [2] = { @@ -484,11 +453,11 @@ describe("'previewpopup'", function() else screen:expect([[ ^ | - {0:~}{1:┌─}{14:foo}{1:─┐}{0: }| - {0:~}{1:│bar │}{0: }| - {0:~}{1:│ │}{0: }| - {0:~}{1:└─────┘}{0: }| - {0:~ }| + {1:~}{4:┌─}{100:foo}{4:─┐}{1: }| + {1:~}{4:│bar │}{1: }| + {1:~}{4:│ │}{1: }| + {1:~}{4:└─────┘}{1: }| + {1:~ }| | ]]) end @@ -500,20 +469,20 @@ describe("'previewpopup'", function() grid = [[ ## grid 1 [5:-------------------]│[2:--------------------]|*5 - {5:[No Name] }{4:[No Name] }| + {2:[No Name] }{3:[No Name] }| [3:----------------------------------------]| ## grid 2 ^ | - {0:~ }|*4 + {1:~ }|*4 ## grid 3 | ## grid 4 - {1:┌}{14: