minimal extui fuzzy finder for neovim
1local cmdline = require("vim._core.ui2.cmdline")
2local ui2 = require("vim._core.ui2")
3
4local _log = {}
5local _loglevel = vim.log.levels.ERROR
6---@private
7---@param msgwrapped { [1]: string, [2]: string? }
8---@param level? integer
9local function logadd(msgwrapped, level)
10 level = level or vim.log.levels.DEBUG
11 if level < _loglevel then
12 return
13 end
14 _log[#_log + 1] = msgwrapped
15end
16---@private
17---@param msg string
18local function logdebug(msg)
19 logadd({ msg .. "\n" })
20end
21---@private
22---@param msg string
23---@param v any
24local function logdbg(msg, v)
25 logdebug(string.format("%s: %s\n", msg, (vim.inspect(v))))
26end
27---@private
28---@param msg string
29local function logerror(msg)
30 logadd({ msg .. "\n", "ErrorMsg" }, vim.log.levels.ERROR)
31end
32
33local prompt_hl_id = vim.api.nvim_get_hl_id_by_name("ArtioPrompt")
34
35---@class artio.View
36---@field picker artio.Picker
37---@field closed boolean
38---@field opts table<'win'|'buf'|'g',table<string,any>>
39---@field marks table<string|integer, integer>
40---@field win artio.View.win
41---@field preview_win integer
42local View = {}
43View.__index = View
44
45---@param picker artio.Picker
46function View:new(picker)
47 ---@diagnostic disable-next-line: undefined-field
48 if picker.log_level then
49 ---@diagnostic disable-next-line: undefined-field
50 _loglevel = picker.log_level
51 end
52
53 return setmetatable({
54 picker = picker,
55 closed = false,
56 opts = {},
57 marks = {},
58 win = {
59 height = 1,
60 },
61 }, View)
62end
63
64---@class artio.View.win
65---@field height integer
66
67local prompthl_id = -1
68
69--- gets updated before draw
70local before_draw_tick = 0
71--- gets updated after changedtick event
72local last_draw_tick = 0
73local function get_changedtick()
74 return vim.api.nvim_buf_get_changedtick(ui2.bufs.cmd)
75end
76
77local cmdbuff = "" ---@type string Stored cmdline used to calculate translation offset.
78local promptlen = 0 -- Current length of the last line in the prompt.
79local promptidx = 0
80--- Concatenate content chunks and set the text for the current row in the cmdline buffer.
81---
82---@param content CmdContent
83---@param prompt string
84function View:setprompttext(content, prompt)
85 local lines = {} ---@type string[]
86 for line in (prompt .. "\n"):gmatch("(.-)\n") do
87 lines[#lines + 1] = vim.fn.strtrans(line)
88 end
89
90 local promptstr = lines[#lines]
91 promptlen = #lines[#lines]
92
93 cmdbuff = ""
94 for _, chunk in ipairs(content) do
95 cmdbuff = cmdbuff .. chunk[2]
96 end
97 lines[#lines] = ("%s%s"):format(promptstr, vim.fn.strtrans(cmdbuff))
98
99 self:promptpos()
100 self:setlines(promptidx, promptidx + 1, lines)
101 if vim.fn.prompt_getprompt(ui2.bufs.cmd) ~= promptstr then
102 vim.fn.prompt_setprompt(ui2.bufs.cmd, promptstr)
103 end
104 vim.schedule(function()
105 local ok, result = pcall(vim.api.nvim_buf_set_mark, ui2.bufs.cmd, ":", promptidx + 1, promptlen, {})
106 if not ok then
107 logerror(("Failed to set mark %d:%d\n\t%s"):format(promptidx, promptlen, result))
108 return
109 end
110 end)
111end
112
113--- Set the cmdline buffer text and cursor position.
114---
115---@param content CmdContent
116---@param pos? integer
117---@param firstc string
118---@param prompt string
119---@param indent integer
120---@param level integer
121---@param hl_id integer
122function View:show(content, pos, firstc, prompt, indent, level, hl_id)
123 cmdline.level, cmdline.indent = level, indent
124 if cmdline.highlighter and cmdline.highlighter.active then
125 cmdline.highlighter.active[ui2.bufs.cmd] = nil
126 end
127 if ui2.msg.cmd.msg_row ~= -1 then
128 ui2.msg.msg_clear()
129 end
130 ui2.msg.virt.last = { {}, {}, {}, {} }
131
132 self:clear()
133 prompthl_id = hl_id
134
135 local cmd_text = ""
136 for _, chunk in ipairs(content) do
137 cmd_text = cmd_text .. chunk[2]
138 end
139
140 self:showmatches()
141
142 self:setprompttext(content, ("%s%s%s"):format(firstc, prompt, (" "):rep(indent)))
143 self:updatecursor(pos)
144
145 self:updatewinheight()
146
147 self:drawprompt()
148 self:hlselect()
149end
150
151--- Set the 'cmdheight' and cmdline window height. Reposition message windows.
152---
153---@param win integer Cmdline window in the current tabpage.
154---@param hide boolean Whether to hide or show the window.
155---@param height integer (Text)height of the cmdline window.
156function View:win_config(win, hide, height)
157 if ui2.cmdheight == 0 and vim.api.nvim_win_get_config(win).hide ~= hide then
158 vim.api.nvim_win_set_config(win, { hide = hide, height = not hide and height or nil })
159 elseif vim.api.nvim_win_get_height(win) ~= height then
160 vim.api.nvim_win_set_height(win, height)
161 end
162
163 if not hide and self.picker.win.hidestatusline then
164 height = 0
165 end
166
167 if vim.o.cmdheight ~= height then
168 -- Avoid moving the cursor with 'splitkeep' = "screen", and altering the user
169 -- configured value with noautocmd.
170 vim._with({ noautocmd = true, o = { splitkeep = "screen" } }, function()
171 vim.o.cmdheight = height
172 end)
173 ui2.msg.set_pos()
174 end
175
176 if self.preview_win and vim.api.nvim_win_is_valid(self.preview_win) then
177 vim.api.nvim_win_set_config(self.preview_win, self:previewconfig())
178 end
179end
180
181---@param predicted? integer The predicted height of the cmdline window
182function View:updatewinheight(predicted)
183 local height = math.max(1, predicted or vim.api.nvim_win_text_height(ui2.wins.cmd, {}).all)
184 height = math.min(height, self.win.height)
185 self:win_config(ui2.wins.cmd, false, height)
186end
187
188function View:saveview()
189 self.save = vim.fn.winsaveview()
190 self.prevwin = vim.api.nvim_get_current_win()
191end
192
193function View:restoreview()
194 vim.api.nvim_set_current_win(self.prevwin)
195 vim.fn.winrestview(self.save)
196end
197
198local ext_winhl = "Search:,CurSearch:,IncSearch:"
199
200---@param restore? boolean
201function View:setopts(restore)
202 local opts = {
203 win = {
204 eventignorewin = "all,-FileType,-InsertCharPre,-TextChangedI,-CursorMovedI",
205 winhighlight = "Normal:ArtioNormal," .. ext_winhl,
206 signcolumn = "no",
207 wrap = false,
208 },
209 buf = {
210 filetype = "artio-picker",
211 buftype = "prompt",
212 autocomplete = false,
213 },
214 g = {
215 showmode = false,
216 showcmd = false,
217 },
218 }
219
220 for level, o in pairs(opts) do
221 self.opts[level] = self.opts[level] or {}
222 local props = {
223 scope = level == "g" and "global" or "local",
224 buf = level == "buf" and ui2.bufs.cmd or nil,
225 win = level == "win" and ui2.wins.cmd or nil,
226 }
227
228 for name, value in pairs(o) do
229 if restore then
230 vim.api.nvim_set_option_value(name, self.opts[level][name], props)
231 else
232 self.opts[level][name] = vim.api.nvim_get_option_value(name, props)
233 vim.api.nvim_set_option_value(name, value, props)
234 end
235 end
236 end
237end
238
239local maxlistheight = 1 -- Max height of the matches list (`self.win.height - 1`)
240
241function View:on_resized()
242 logdebug("on_resized")
243
244 if self.picker.win.height > 1 then
245 self.win.height = self.picker.win.height
246 else
247 self.win.height = vim.o.lines * self.picker.win.height
248 end
249 self.win.height = math.max(math.ceil(self.win.height), 1)
250
251 maxlistheight = math.max(self.win.height - 1, 1)
252end
253
254function View:open()
255 if not self.picker then
256 return
257 end
258 _log = nil
259 _log = {}
260
261 ui2.check_targets()
262
263 vim.schedule(function()
264 self.augroup = vim.api.nvim_create_augroup("artio:group", { clear = true })
265
266 vim.api.nvim_create_autocmd("CmdlineLeave", {
267 group = self.augroup,
268 once = true,
269 callback = function()
270 self:close()
271 end,
272 })
273
274 vim.api.nvim_create_autocmd("ModeChanged", {
275 group = self.augroup,
276 callback = function(ev)
277 if string.match(ev.match, "^i:") then
278 self:close()
279 end
280 end,
281 })
282
283 vim.api.nvim_create_autocmd({ "VimResized", "WinEnter" }, {
284 group = self.augroup,
285 callback = function()
286 self:on_resized()
287 end,
288 })
289
290 vim.api.nvim_create_autocmd("WinEnter", {
291 group = self.augroup,
292 callback = function()
293 self:update(true)
294 end,
295 })
296
297 vim.api.nvim_create_autocmd("TextChangedI", {
298 group = self.augroup,
299 buffer = ui2.bufs.cmd,
300 callback = function()
301 self:update()
302 end,
303 })
304
305 vim.api.nvim_create_autocmd("CursorMovedI", {
306 group = self.augroup,
307 buffer = ui2.bufs.cmd,
308 callback = function()
309 self:updatecursor()
310 end,
311 })
312 end)
313
314 cmdline.prompt = false
315 cmdline.srow = 0
316 cmdline.indent = 1
317 cmdline.level = 1
318
319 self:saveview()
320
321 -- initial render
322 self:trigger_show()
323
324 vim._with({ noautocmd = true }, function()
325 vim.api.nvim_set_current_win(ui2.wins.cmd)
326 end)
327
328 self:setopts()
329
330 -- start insert *before* registering events
331 self:updatecursor()
332 vim._with({ noautocmd = true }, function()
333 vim.cmd.startinsert({ bang = true })
334 end)
335
336 -- trigger after registering events
337 vim.schedule(function()
338 vim._with({ win = ui2.wins.cmd, wo = { eventignorewin = "" } }, function()
339 vim.api.nvim_exec_autocmds("WinEnter", {})
340 end)
341 end)
342end
343
344function View:close()
345 if self.closed then
346 return
347 end
348 self:closepreview()
349 vim.schedule(function()
350 pcall(vim.api.nvim_del_augroup_by_id, self.augroup)
351 pcall(vim.api.nvim_buf_detach, ui2.bufs.cmd)
352
353 vim.cmd.stopinsert()
354
355 -- prepare state
356 self:setopts(true)
357
358 -- reset state
359 self:clear()
360 cmdline.srow = 0
361 cmdline.erow = 0
362
363 -- restore ui
364 self:hide()
365 self:restoreview()
366 vim.cmd.redraw()
367
368 self.closed = true
369
370 self.picker:close()
371
372 vim.api.nvim_echo(_log, true, {})
373 end)
374end
375
376function View:hide()
377 vim.fn.clearmatches(ui2.wins.cmd) -- Clear matchparen highlights.
378 vim.api.nvim_win_set_cursor(ui2.wins.cmd, { 1, 0 })
379 vim.api.nvim_buf_set_lines(ui2.bufs.cmd, 0, -1, false, {})
380
381 local clear = vim.schedule_wrap(function(was_prompt)
382 -- Avoid clearing prompt window when it is re-entered before the next event
383 -- loop iteration. E.g. when a non-choice confirm button is pressed.
384 if was_prompt and not cmdline.prompt then
385 pcall(function()
386 vim.api.nvim_buf_set_lines(ui2.bufs.cmd, 0, -1, false, {})
387 vim.api.nvim_buf_set_lines(ui2.bufs.dialog, 0, -1, false, {})
388 vim.api.nvim_win_set_config(ui2.wins.dialog, { hide = true })
389 vim.on_key(nil, ui2.msg.dialog_on_key)
390 end)
391 end
392 -- Messages emitted as a result of a typed command are treated specially:
393 -- remember if the cmdline was used this event loop iteration.
394 -- NOTE: Message event callbacks are themselves scheduled, so delay two iterations.
395 vim.schedule(function()
396 cmdline.level = -1
397 end)
398 end)
399 clear(cmdline.prompt)
400
401 cmdline.prompt, cmdline.level = false, 0
402 self:win_config(ui2.wins.cmd, true, ui2.cmdheight)
403end
404
405function View:trigger_show()
406 logdebug("trigger_show")
407 local input
408 if self.picker.live then
409 input = self.picker.liveinput
410 else
411 input = self.picker.input
412 end
413 self:show({ { 0, input } }, -1, "", self.picker.prompttext, cmdline.indent, cmdline.level, prompt_hl_id)
414end
415
416---@param force? boolean
417function View:update(force)
418 if not force and before_draw_tick < last_draw_tick and before_draw_tick == get_changedtick() - 1 then
419 logdebug("update (skip-redraw)")
420 return self:drawprompt()
421 end
422
423 logdebug("update")
424
425 local text = vim.api.nvim_get_current_line()
426 text = text:sub(promptlen + 1)
427
428 if self.picker.live then
429 self.picker.liveinput = text
430 else
431 self.picker.input = text
432 end
433
434 vim.schedule(coroutine.wrap(function()
435 logdebug("getmatches")
436 self.picker:getmatches()
437
438 if self.closed then
439 return
440 end
441
442 vim.schedule_wrap(self.trigger_show)(self)
443 end))
444end
445
446local curpos = { 0, 0 } -- Last drawn cursor position. absolute
447---@param pos? integer relative to prompt
448function View:updatecursor(pos)
449 logdebug("updatecursor")
450
451 self:promptpos()
452
453 if not pos or pos < 0 then
454 local cursorpos = vim.api.nvim_win_get_cursor(ui2.wins.cmd)
455 pos = cursorpos[2] - promptlen
456 end
457
458 curpos[2] = math.max(curpos[2], promptlen)
459
460 if curpos[1] == promptidx + 1 and curpos[2] == promptlen + pos then
461 return
462 end
463
464 if pos < 0 then
465 -- reset to last known position
466 pos = curpos[2] - promptlen
467 end
468
469 curpos[1], curpos[2] = promptidx + 1, promptlen + pos
470
471 vim._with({ noautocmd = true }, function()
472 local ok, _ = pcall(vim.api.nvim_win_set_cursor, ui2.wins.cmd, curpos)
473 if not ok then
474 logerror(("Failed to set cursor %d:%d"):format(curpos[1], curpos[2]))
475 end
476 end)
477end
478
479local srow = 0
480
481function View:clear()
482 srow = self.picker.opts.bottom and 0 or 1
483 cmdline.erow = srow
484 self:setlines(0, -1, {})
485end
486
487function View:promptpos()
488 promptidx = self.picker.opts.bottom and cmdline.erow or 0
489end
490
491function View:setlines(posstart, posend, lines)
492 -- update winheight to prevent wrong scroll when increasing from 1
493 local diff = #lines - (posend - posstart)
494 if diff ~= 0 then
495 local height = vim.api.nvim_win_text_height(ui2.wins.cmd, {}).all
496 local predicted = height + diff
497 self:updatewinheight(predicted)
498 end
499
500 before_draw_tick = get_changedtick()
501 vim.api.nvim_buf_set_lines(ui2.bufs.cmd, posstart, posend, false, lines)
502 last_draw_tick = get_changedtick()
503end
504
505local view_ns = vim.api.nvim_create_namespace("artio:view:ns")
506local ext_priority = {
507 prompt = 1,
508 info = 2,
509 select = 4,
510 marker = 8,
511 hl = 16,
512 icon = 32,
513 match = 64,
514}
515
516---@param id? string|integer
517---@param line integer 0-based
518---@param col integer 0-based
519---@param opts vim.api.keyset.set_extmark
520---@return integer
521function View:mark(id, line, col, opts)
522 if id and self.marks[id] then
523 vim._with({ noautocmd = true }, function()
524 vim.api.nvim_buf_del_extmark(ui2.bufs.cmd, view_ns, self.marks[id])
525 end)
526 self.marks[id] = nil
527 end
528
529 opts.hl_mode = "combine"
530 opts.invalidate = true
531
532 local ok, result
533 vim._with({ noautocmd = true }, function()
534 ok, result = pcall(vim.api.nvim_buf_set_extmark, ui2.bufs.cmd, view_ns, line, col, opts)
535 end)
536 if not ok then
537 logerror(("Failed to add extmark %d:%d\n\t%s"):format(line, col, result))
538 return -1
539 end
540
541 if id and result >= 0 then
542 self.marks[id] = result
543 end
544
545 return result
546end
547
548---@param p artio.Picker
549---@param info 'index'|'list'|string
550---@return string
551local function getpromptinfo(p, info)
552 if info == "index" then
553 return ("[%d]"):format(p.idx)
554 elseif info == "list" then
555 return ("(%d/%d)"):format(#p.matches, #p.items)
556 end
557 return ""
558end
559
560function View:drawprompt()
561 logdebug("drawprompt")
562
563 self:promptpos()
564 if promptlen > 0 and prompthl_id > 0 then
565 self:mark("prompthl", promptidx, 0, { hl_group = prompthl_id, end_col = promptlen, priority = ext_priority.prompt })
566 self:mark("promptinfo", promptidx, 0, {
567 virt_text = {
568 {
569 table.concat(
570 vim
571 .iter(self.picker.opts.infolist)
572 :map(function(info)
573 return getpromptinfo(self.picker, info)
574 end)
575 :totable(),
576 " "
577 ),
578 "InfoText",
579 },
580 },
581 virt_text_pos = "eol_right_align",
582 priority = ext_priority.info,
583 })
584 end
585end
586
587local offset = 0
588
589function View:updateoffset()
590 self.picker:fix()
591 if self.picker.idx == 0 then
592 offset = 0
593 return
594 end
595
596 local _offset = self.picker.idx - maxlistheight
597 if _offset > offset then
598 offset = _offset
599 elseif self.picker.idx <= offset then
600 offset = self.picker.idx - 1
601 end
602
603 offset = math.min(math.max(0, offset), math.max(0, #self.picker.matches - maxlistheight))
604end
605
606local icon_pad = 2
607
608function View:showmatches()
609 local indent = vim.fn.strdisplaywidth(self.picker.opts.pointer) + 1
610 local prefix = (" "):rep(indent)
611 local icon_pad_str = (" "):rep(icon_pad)
612
613 self:updateoffset()
614
615 local lines = {} ---@type string[]
616 local hls = {}
617 local icons = {} ---@type ([string, string]|false)[]
618 local custom_hls = {} ---@type (artio.Picker.hl[]|false)[]
619 local marks = {} ---@type boolean[]
620 for i = 1 + offset, math.min(#self.picker.matches, maxlistheight + offset) do
621 local match = self.picker.matches[i]
622 local item = self.picker.items[match[1]]
623
624 local icon, icon_hl = item.icon, item.icon_hl
625 if not (icon and icon_hl) and vim.is_callable(self.picker.get_icon) then
626 icon, icon_hl = self.picker.get_icon(item)
627 item.icon, item.icon_hl = icon, icon_hl
628 end
629 icons[#icons + 1] = icon and { icon, icon_hl } or false
630 icon = icon and ("%s%s"):format(item.icon, icon_pad_str) or ""
631
632 local hl = item.hls
633 if not hl and vim.is_callable(self.picker.hl_item) then
634 hl = self.picker.hl_item(item)
635 item.hls = hl
636 end
637 custom_hls[#custom_hls + 1] = hl or false
638
639 marks[#marks + 1] = self.picker.marked[item.id] or false
640
641 lines[#lines + 1] = ("%s%s%s"):format(prefix, icon, item.text)
642 hls[#hls + 1] = match[2]
643 end
644
645 if not self.picker.opts.shrink then
646 for _ = 1, (maxlistheight - #lines) do
647 lines[#lines + 1] = ""
648 end
649 end
650 self:setlines(srow, cmdline.erow, lines)
651 cmdline.erow = srow + #lines
652
653 for i = 1, #lines do
654 local has_icon = icons[i] and icons[i][1] and true
655 local icon_indent = has_icon and (#icons[i][1] + icon_pad) or 0
656
657 if has_icon and icons[i][2] then
658 self:mark(nil, srow + i - 1, indent, {
659 end_col = indent + icon_indent,
660 hl_group = icons[i][2],
661 priority = ext_priority.icon,
662 })
663 end
664
665 local line_hls = custom_hls[i]
666 if line_hls then
667 for j = 1, #line_hls do
668 local hl = line_hls[j]
669 self:mark(nil, srow + i - 1, indent + icon_indent + hl[1][1], {
670 end_col = indent + icon_indent + hl[1][2],
671 hl_group = hl[2],
672 priority = ext_priority.hl,
673 })
674 end
675 end
676
677 if marks[i] then
678 self:mark(nil, srow + i - 1, indent - 1, {
679 virt_text = { { self.picker.opts.marker, "ArtioMark" } },
680 virt_text_pos = "overlay",
681 priority = ext_priority.marker,
682 })
683 self:mark(nil, srow + i - 1, 0, {
684 hl_group = "ArtioMarkLine",
685 hl_eol = true,
686 end_row = srow + i,
687 end_col = 0,
688
689 priority = ext_priority.marker,
690 })
691 end
692
693 if hls[i] then
694 for j = 1, #hls[i] do
695 local col = indent + icon_indent + hls[i][j]
696 self:mark(nil, srow + i - 1, col, {
697 hl_group = "ArtioMatch",
698 end_col = col + 1,
699 priority = ext_priority.match,
700 })
701 end
702 end
703 end
704end
705
706function View:hlselect()
707 self:softupdatepreview()
708
709 self.picker:fix()
710 local idx = self.picker.idx
711 if idx == 0 then
712 return
713 end
714
715 self:updateoffset()
716 local row = math.max(0, math.min(srow + (idx - offset), cmdline.erow) - 1)
717
718 self:mark("hlselect", row, 0, {
719 virt_text = { { self.picker.opts.pointer, "ArtioPointer" } },
720 virt_text_pos = "overlay",
721
722 hl_group = "ArtioSel",
723 hl_eol = true,
724 end_row = row + 1,
725 end_col = 0,
726
727 priority = ext_priority.select,
728 })
729end
730
731function View:togglepreview()
732 if self.preview_win then
733 self:closepreview()
734 return
735 end
736
737 self:updatepreview()
738end
739
740---@return integer
741---@return fun(win: integer)?
742function View:openpreview()
743 if self.picker.idx == 0 then
744 return -1
745 end
746
747 local match = self.picker.matches[self.picker.idx]
748 local item = self.picker.items[match[1]]
749
750 if not item or not (self.picker.preview_item and vim.is_callable(self.picker.preview_item)) then
751 return -1
752 end
753
754 return self.picker.preview_item(item.v)
755end
756
757function View:previewconfig()
758 local previewopts = self.picker.win.preview_opts
759 and vim.is_callable(self.picker.win.preview_opts)
760 and self.picker.win.preview_opts(self)
761 local cmdheight = vim.api.nvim_win_get_height(ui2.wins.cmd)
762
763 local winborder = previewopts and previewopts.border or vim.o.winborder
764 return vim.tbl_extend("force", {
765 relative = "editor",
766 width = vim.o.columns,
767 height = self.win.height,
768 col = 0,
769 row = vim.o.lines
770 - (self.win.height + cmdheight)
771 - ((winborder == "none" or winborder == "") and 0 or 2)
772 - (self.picker.win.hidestatusline and 0 or 1),
773 }, previewopts or {})
774end
775
776function View:updatepreview()
777 local buf, on_win = self:openpreview()
778 if buf < 0 then
779 return
780 end
781
782 if not self.preview_win then
783 self.preview_win = vim.api.nvim_open_win(buf, false, self:previewconfig())
784 else
785 vim.api.nvim_win_set_buf(self.preview_win, buf)
786 end
787
788 vim._with({ win = self.preview_win, noautocmd = true }, function()
789 vim.api.nvim_set_option_value("previewwindow", true, { scope = "local" })
790 vim.api.nvim_set_option_value("eventignorewin", "all,-FileType", { scope = "local" })
791 end)
792
793 if on_win and vim.is_callable(on_win) then
794 on_win(self.preview_win)
795 end
796end
797
798function View:softupdatepreview()
799 if self.picker.idx == 0 then
800 self:closepreview()
801 end
802
803 if not self.preview_win then
804 return
805 end
806
807 self:updatepreview()
808end
809
810function View:closepreview()
811 if not self.preview_win then
812 return
813 end
814
815 vim.api.nvim_win_close(self.preview_win, true)
816 self.preview_win = nil
817end
818
819return View