diff --git a/script/core/completion.lua b/script/core/completion.lua
index 06219e74963235f0e1e1df29bb2badd5d279dd58..dc4ddf28ede376b8c4bee41881a4f7f7e012ca7c 100644
--- a/script/core/completion.lua
+++ b/script/core/completion.lua
@@ -191,13 +191,7 @@ local function buildDesc(source)
         return
     end
     local hover = getHover.get(source)
-    local md = markdown()
-    md:add('lua', hover.label)
-    md:splitLine()
-    md:add('md',  hover.description)
-    md:splitLine()
-    md:add('lua', getSnip(source))
-    return md
+    return hover
 end
 
 local function buildFunction(results, source, value, oop, data)
diff --git a/script/core/hover/init.lua b/script/core/hover/init.lua
index c6c2d92ee5246479bf8365dd7d32c92d00ee42af..6e98d6c9ef56295e6af9234656528a44a18a1f02 100644
--- a/script/core/hover/init.lua
+++ b/script/core/hover/init.lua
@@ -1,145 +1,55 @@
 local files      = require 'files'
-local searcher   = require 'core.searcher'
 local vm         = require 'vm'
 local getLabel   = require 'core.hover.label'
 local getDesc    = require 'core.hover.description'
 local util       = require 'utility'
 local findSource = require 'core.find-source'
-local lang       = require 'language'
 local markdown   = require 'provider.markdown'
 local infer      = require 'core.infer'
 
-local function eachFunctionAndOverload(value, callback)
-    callback(value)
-    if not value.bindDocs then
-        return
-    end
-    for _, doc in ipairs(value.bindDocs) do
-        if doc.type == 'doc.overload' then
-            callback(doc.overload)
-        end
-    end
-end
+local function getHover(source)
+    local md        = markdown()
+    local defMark   = {}
+    local labelMark = {}
+    local descMark  = {}
 
-local function getHoverAsValue(source)
-    local label = getLabel(source)
-    local desc  = getDesc(source)
-    if not desc then
-        local values = vm.getDefs(source)
-        for _, def in ipairs(values) do
-            desc = getDesc(def)
-            if desc then
-                break
-            end
+    local function addHover(def)
+        if defMark[def] then
+            return
         end
-    end
-    return {
-        label       = label,
-        source      = source,
-        description = desc,
-    }
-end
+        defMark[def] = true
 
-local function getHoverAsFunction(source)
-    local values = vm.getDefs(source)
-    local desc   = getDesc(source)
-    local labels = {}
-    local defs = 0
-    local protos = 0
-    local other = 0
-    local mark = {}
-    for _, def in ipairs(values) do
-        def = searcher.getObjectValue(def) or def
-        if def.type == 'function'
-        or def.type == 'doc.type.function' then
-            eachFunctionAndOverload(def, function (value)
-                if mark[value] then
-                    return
-                end
-                mark[value] =true
-                local label = getLabel(value)
-                if label then
-                    defs = defs + 1
-                    labels[label] = (labels[label] or 0) + 1
-                    if labels[label] == 1 then
-                        protos = protos + 1
-                    end
-                end
-                desc = desc or getDesc(value)
-            end)
-        elseif def.type == 'table'
-        or     def.type == 'boolean'
-        or     def.type == 'string'
-        or     def.type == 'integer'
-        or     def.type == 'number' then
-            other = other + 1
-            desc = desc or getDesc(def)
-        end
-    end
+        local label = getLabel(def)
+        local desc  = getDesc(def)
 
-    if defs == 0 then
-        return getHoverAsValue(source)
-    end
+        if not labelMark[tostring(label)] then
+            labelMark[tostring(label)] = true
+            md:add('lua', label)
+            md:splitLine()
+        end
 
-    if defs == 1 and other == 0 then
-        return {
-            label       = next(labels),
-            source      = source,
-            description = desc,
-        }
+        if not descMark[tostring(desc)] then
+            descMark[tostring(desc)] = true
+            md:add('md', desc)
+            md:splitLine()
+        end
     end
 
-    local lines = {}
-    if defs > 1 then
-        lines[#lines+1] = lang.script('HOVER_MULTI_DEF_PROTO', defs, protos)
-    end
-    if other > 0 then
-        lines[#lines+1] = lang.script('HOVER_MULTI_PROTO_NOT_FUNC', other)
-    end
-    if defs > 1 then
-        for label, count in util.sortPairs(labels) do
-            lines[#lines+1] = ('(%d) %s'):format(count, label)
+    if infer.searchAndViewInfers(source) == 'function' then
+        for _, def in ipairs(vm.getDefs(source)) do
+            if def.type == 'function'
+            or def.type == 'doc.type.function' then
+                addHover(def)
+            end
         end
     else
-        lines[#lines+1] = next(labels)
-    end
-    local label = table.concat(lines, '\n')
-    return {
-        label       = label,
-        source      = source,
-        description = desc,
-    }
-end
-
-local function getHoverAsDocName(source)
-    local label = getLabel(source)
-    local desc  = getDesc(source)
-    return {
-        label       = label,
-        source      = source,
-        description = desc,
-    }
-end
-
-local function isFunction(source)
-    local defs = vm.getAllDefs(source)
-    for _, def in ipairs(defs) do
-        if def.type == 'function' then
-            return true
+        addHover(source)
+        for _, def in ipairs(vm.getDefs(source)) do
+            addHover(def)
         end
     end
-    return false
-end
 
-local function getHover(source)
-    if source.type == 'doc.type.name' then
-        return getHoverAsDocName(source)
-    end
-    if isFunction(source) then
-        return getHoverAsFunction(source)
-    else
-        return getHoverAsValue(source)
-    end
+    return md
 end
 
 local accept = {
@@ -168,14 +78,12 @@ local function getHoverByUri(uri, offset)
     end
     local hover = getHover(source)
     if SHOWSOURCE then
-        hover.description = ('%s\n---\n\n```lua\n%s\n```'):format(
-            hover.description or '',
-            util.dump(source, {
-                deep = 1,
-            })
-        )
+        hover:splitLine()
+        hover:add('lua', util.dump(source, {
+            deep = 1,
+        }))
     end
-    return hover
+    return hover, source
 end
 
 return {
diff --git a/script/provider/markdown.lua b/script/provider/markdown.lua
index 140267d705905520b955621597eea4203054e6b0..3a215f0f01842a5582e46de1ab861af9cc624132 100644
--- a/script/provider/markdown.lua
+++ b/script/provider/markdown.lua
@@ -15,6 +15,7 @@ function mt:add(language, text)
     if not text then
         return
     end
+    self._cacheResult = nil
     if type(text) == 'table' then
         self[#self+1] = {
             type     = 'markdown',
@@ -34,12 +35,16 @@ function mt:add(language, text)
 end
 
 function mt:splitLine()
+    self._cacheResult = nil
     self[#self+1] = {
         type = 'splitline',
     }
 end
 
 function mt:string()
+    if self._cacheResult then
+        return self._cacheResult
+    end
     local lines = {}
     local language = 'md'
 
@@ -97,7 +102,9 @@ function mt:string()
         end
     end
 
-    return table.concat(lines, '\n')
+    local result = table.concat(lines, '\n')
+    self._cacheResult = result
+    return result
 end
 
 return function ()
diff --git a/script/provider/provider.lua b/script/provider/provider.lua
index 09a031ccb8dbde43e4e9df0ec1b645f59561f323..6ffb0837308795c92d100eb46ca87ceb2d690523 100644
--- a/script/provider/provider.lua
+++ b/script/provider/provider.lua
@@ -186,20 +186,16 @@ proto.on('textDocument/hover', function (params)
         return nil
     end
     local offset = files.offsetOfWord(uri, params.position)
-    local hover = core.byUri(uri, offset)
+    local hover, source = core.byUri(uri, offset)
     if not hover then
         return nil
     end
-    local md = markdown()
-    md:add('lua', hover.label)
-    md:splitLine()
-    md:add('md',  hover.description)
     return {
         contents = {
-            value = md:string(),
+            value = tostring(hover),
             kind  = 'markdown',
         },
-        range = files.range(uri, hover.source.start, hover.source.finish),
+        range = files.range(uri, source.start, source.finish),
     }
 end)
 
diff --git a/test/crossfile/hover.lua b/test/crossfile/hover.lua
index 04f7cc02ee95423f5d334bce1560146dadc7e6dd..087739593c47e6fac9d78082719491719433142d 100644
--- a/test/crossfile/hover.lua
+++ b/test/crossfile/hover.lua
@@ -68,14 +68,8 @@ function TEST(expect)
     local sourcePos = (sourceList[1][1] + sourceList[1][2]) // 2
     local hover = core.byUri(sourceUri, sourcePos)
     assert(hover)
-    if hover.label then
-        hover.label = hover.label:gsub('\r\n', '\n')
-    end
-    if hover.description then
-        hover.description = tostring(hover.description)
-    end
-    assert(eq(hover.label, expect.hover.label))
-    assert(eq(hover.description, expect.hover.description))
+    hover = tostring(hover):gsub('\r\n', '\n')
+    assert(eq(hover, expect.hover))
 end
 
 TEST {
@@ -87,10 +81,13 @@ TEST {
         path = 'b.lua',
         content = 'require <?"a"?>',
     },
-    hover = {
-        label = '1 个字节',
-        description = [[* [a.lua](file:///a.lua) (搜索路径: `?.lua`)]],
-    }
+    hover = [[
+```lua
+1 个字节
+```
+
+---
+* [a.lua](file:///a.lua) (搜索路径: `?.lua`)]],
 }
 
 if require 'bee.platform'.OS == 'Windows' then
diff --git a/test/hover/init.lua b/test/hover/init.lua
index 62db4b6d69e535345ebaaf40cb00032bffc141f2..3f07ea309adb2f61e6fba064db30ed007af58437 100644
--- a/test/hover/init.lua
+++ b/test/hover/init.lua
@@ -1,8 +1,25 @@
-local core  = require 'core.hover'
-local files = require 'files'
+local core       = require 'core.hover'
+local findSource = require 'core.find-source'
+local getLabel   = require 'core.hover.label'
+local files      = require 'files'
 
 rawset(_G, 'TEST', true)
 
+local accept = {
+    ['local']         = true,
+    ['setlocal']      = true,
+    ['getlocal']      = true,
+    ['setglobal']     = true,
+    ['getglobal']     = true,
+    ['field']         = true,
+    ['method']        = true,
+    ['string']        = true,
+    ['number']        = true,
+    ['integer']       = true,
+    ['doc.type.name'] = true,
+    ['function']      = true,
+}
+
 function TEST(script)
     return function (expect)
         files.removeAll()
@@ -14,7 +31,7 @@ function TEST(script)
         local hover = core.byUri('', pos)
         assert(hover)
         expect = expect:gsub('^[\r\n]*(.-)[\r\n]*$', '%1'):gsub('\r\n', '\n')
-        local label = hover.label:gsub('^[\r\n]*(.-)[\r\n]*$', '%1'):gsub('\r\n', '\n')
+        local label = tostring(hover):match('```lua[\r\n]*(.-)[\r\n]*```'):gsub('\r\n', '\n')
         assert(expect == label)
     end
 end
@@ -571,9 +588,7 @@ end
 <?F?>()
 ]]
 [[
-(3 个定义,2 个原型)
-(2) function F(a: any)
-(1) function F(b: any)
+function F(a: any)
 ]]
 
 -- 不根据参数推断
@@ -1278,9 +1293,7 @@ function f(x, y, z) end
 print(<?f?>)
 ]]
 [[
-(2 个定义,2 个原型)
-(1) function f(x: number, y: boolean, z: string)
-(1) function f(y: boolean)
+function f(x: number, y: boolean, z: string)
 ]]
 
 TEST [[
@@ -1397,8 +1410,7 @@ local t = {}
 function t.<?f?>() end
 ]]
 [[
-(2 个定义,1 个原型)
-(2) function c.f()
+function c.f()
 ]]
 
 TEST [[
@@ -1409,8 +1421,7 @@ t = {}
 function t.<?f?>() end
 ]]
 [[
-(2 个定义,1 个原型)
-(2) function t.f()
+function t.f()
 ]]
 
 TEST [[