From 9f25c279f8162098d7dfe000533bfbbb8702e024 Mon Sep 17 00:00:00 2001
From: "Wu, Zhenyu" <wuzhenyu@ustc.edu>
Date: Thu, 19 Dec 2024 16:09:54 +0800
Subject: [PATCH 01/34] Exit 1 when check error

---
 script/cli/check.lua        | 157 ++++++++++++------------
 script/cli/check_worker.lua | 233 +++++++++++++++++++-----------------
 script/cli/init.lua         |   8 +-
 3 files changed, 206 insertions(+), 192 deletions(-)

diff --git a/script/cli/check.lua b/script/cli/check.lua
index c3aac0e3d..c9a23b02c 100644
--- a/script/cli/check.lua
+++ b/script/cli/check.lua
@@ -5,97 +5,104 @@ local json       = require 'json'
 local jsonb      = require 'json-beautify'
 local util       = require 'utility'
 
+local export = {}
 
-local numThreads = tonumber(NUM_THREADS or 1)
+function export.runCLI()
+    local numThreads = tonumber(NUM_THREADS or 1)
 
-local exe
-local minIndex = -1
-while arg[minIndex] do
-    exe = arg[minIndex]
-    minIndex = minIndex - 1
-end
--- TODO: is this necessary? got it from the shell.lua helper in bee.lua tests
-if platform.os == 'windows' and not exe:match('%.[eE][xX][eE]$') then
-    exe = exe..'.exe'
-end
+    local exe
+    local minIndex = -1
+    while arg[minIndex] do
+        exe = arg[minIndex]
+        minIndex = minIndex - 1
+    end
+    -- TODO: is this necessary? got it from the shell.lua helper in bee.lua tests
+    if platform.os == 'windows' and not exe:match('%.[eE][xX][eE]$') then
+        exe = exe..'.exe'
+    end
 
-local function logFileForThread(threadId)
-    return LOGPATH .. '/check-partial-' .. threadId .. '.json'
-end
+    local function logFileForThread(threadId)
+        return LOGPATH .. '/check-partial-' .. threadId .. '.json'
+    end
 
-local function buildArgs(threadId)
-    local args = {exe}
-    local skipNext = false
-    for i = 1, #arg do
-        local arg = arg[i]
-        -- --check needs to be transformed into --check_worker
-        if arg:lower():match('^%-%-check$') or arg:lower():match('^%-%-check=') then
-            args[#args + 1] = arg:gsub('%-%-%w*', '--check_worker')
-        -- --check_out_path needs to be removed if we have more than one thread
-        elseif arg:lower():match('%-%-check_out_path') and numThreads > 1 then
-            if not arg:match('%-%-%w*=') then
-                skipNext = true
-            end
-        else
-            if skipNext then
-                skipNext = false
+    local function buildArgs(threadId)
+        local args = {exe}
+        local skipNext = false
+        for i = 1, #arg do
+            local arg = arg[i]
+            -- --check needs to be transformed into --check_worker
+            if arg:lower():match('^%-%-check$') or arg:lower():match('^%-%-check=') then
+                args[#args + 1] = arg:gsub('%-%-%w*', '--check_worker')
+            -- --check_out_path needs to be removed if we have more than one thread
+            elseif arg:lower():match('%-%-check_out_path') and numThreads > 1 then
+                if not arg:match('%-%-%w*=') then
+                    skipNext = true
+                end
             else
-                args[#args + 1] = arg
+                if skipNext then
+                    skipNext = false
+                else
+                    args[#args + 1] = arg
+                end
             end
         end
+        args[#args + 1] = '--thread_id'
+        args[#args + 1] = tostring(threadId)
+        if numThreads > 1 then
+            args[#args + 1] = '--quiet'
+            args[#args + 1] = '--check_out_path'
+            args[#args + 1] = logFileForThread(threadId)
+        end
+        return args
     end
-    args[#args + 1] = '--thread_id'
-    args[#args + 1] = tostring(threadId)
+
     if numThreads > 1 then
-        args[#args + 1] = '--quiet'
-        args[#args + 1] = '--check_out_path'
-        args[#args + 1] = logFileForThread(threadId)
+        print(lang.script('CLI_CHECK_MULTIPLE_WORKERS', numThreads))
     end
-    return args
-end
 
-if numThreads > 1 then
-    print(lang.script('CLI_CHECK_MULTIPLE_WORKERS', numThreads))
-end
-
-local procs = {}
-for i = 1, numThreads do
-    local process, err = subprocess.spawn({buildArgs(i)})
-    if err then
-        print(err)
-    end
-    if process then
-        procs[#procs + 1] = process
+    local procs = {}
+    for i = 1, numThreads do
+        local process, err = subprocess.spawn({buildArgs(i)})
+        if err then
+            print(err)
+        end
+        if process then
+            procs[#procs + 1] = process
+        end
     end
-end
 
-for _, process in ipairs(procs) do
-    process:wait()
-end
+    local checkPassed = true
+    for _, process in ipairs(procs) do
+        checkPassed = process:wait() == 0 and checkPassed
+    end
 
-local outpath = CHECK_OUT_PATH
-if outpath == nil then
-    outpath = LOGPATH .. '/check.json'
-end
+    local outpath = CHECK_OUT_PATH
+    if outpath == nil then
+        outpath = LOGPATH .. '/check.json'
+    end
 
-if numThreads > 1 then
-    local mergedResults = {}
-    local count = 0
-    for i = 1, numThreads do
-        local result = json.decode(util.loadFile(logFileForThread(i)) or '[]')
-        for k, v in pairs(result) do
-            local entries = mergedResults[k] or {}
-            mergedResults[k] = entries
-            for _, entry in ipairs(v) do
-                entries[#entries + 1] = entry
-                count = count + 1
+    if numThreads > 1 then
+        local mergedResults = {}
+        local count = 0
+        for i = 1, numThreads do
+            local result = json.decode(util.loadFile(logFileForThread(i)) or '[]')
+            for k, v in pairs(result) do
+                local entries = mergedResults[k] or {}
+                mergedResults[k] = entries
+                for _, entry in ipairs(v) do
+                    entries[#entries + 1] = entry
+                    count = count + 1
+                end
             end
         end
+        util.saveFile(outpath, jsonb.beautify(mergedResults))
+        if count == 0 then
+            print(lang.script('CLI_CHECK_SUCCESS'))
+        else
+            print(lang.script('CLI_CHECK_RESULTS', count, outpath))
+        end
     end
-    util.saveFile(outpath, jsonb.beautify(mergedResults))
-    if count == 0 then
-        print(lang.script('CLI_CHECK_SUCCESS'))
-    else
-        print(lang.script('CLI_CHECK_RESULTS', count, outpath))
-    end
+    return checkPassed and 0 or 1
 end
+
+return export
diff --git a/script/cli/check_worker.lua b/script/cli/check_worker.lua
index 822c83dc6..a2e0bff11 100644
--- a/script/cli/check_worker.lua
+++ b/script/cli/check_worker.lua
@@ -15,145 +15,152 @@ local await     = require 'await'
 require 'plugin'
 require 'vm'
 
-lang(LOCALE)
+local export = {}
 
-local numThreads = tonumber(NUM_THREADS or 1)
-local threadId = tonumber(THREAD_ID or 1)
+function export.runCLI()
+    lang(LOCALE)
 
-if type(CHECK_WORKER) ~= 'string' then
-    print(lang.script('CLI_CHECK_ERROR_TYPE', type(CHECK_WORKER)))
-    return
-end
+    local numThreads = tonumber(NUM_THREADS or 1)
+    local threadId = tonumber(THREAD_ID or 1)
 
-local rootPath = fs.absolute(fs.path(CHECK_WORKER)):string()
-local rootUri = furi.encode(rootPath)
-if not rootUri then
-    print(lang.script('CLI_CHECK_ERROR_URI', rootPath))
-    return
-end
-rootUri = rootUri:gsub("/$", "")
+    if type(CHECK_WORKER) ~= 'string' then
+        print(lang.script('CLI_CHECK_ERROR_TYPE', type(CHECK_WORKER)))
+        return
+    end
 
-if CHECKLEVEL then
-    if not define.DiagnosticSeverity[CHECKLEVEL] then
-        print(lang.script('CLI_CHECK_ERROR_LEVEL', 'Error, Warning, Information, Hint'))
+    local rootPath = fs.absolute(fs.path(CHECK_WORKER)):string()
+    local rootUri = furi.encode(rootPath)
+    if not rootUri then
+        print(lang.script('CLI_CHECK_ERROR_URI', rootPath))
         return
     end
-end
-local checkLevel = define.DiagnosticSeverity[CHECKLEVEL] or define.DiagnosticSeverity.Warning
+    rootUri = rootUri:gsub("/$", "")
 
-util.enableCloseFunction()
+    if CHECKLEVEL then
+        if not define.DiagnosticSeverity[CHECKLEVEL] then
+            print(lang.script('CLI_CHECK_ERROR_LEVEL', 'Error, Warning, Information, Hint'))
+            return
+        end
+    end
+    local checkLevel = define.DiagnosticSeverity[CHECKLEVEL] or define.DiagnosticSeverity.Warning
 
-local lastClock = os.clock()
-local results = {}
+    util.enableCloseFunction()
 
-local function errorhandler(err)
-    print(err)
-    print(debug.traceback())
-end
+    local lastClock = os.clock()
+    local results = {}
 
----@async
-xpcall(lclient.start, errorhandler, lclient, function (client)
-    await.disable()
-    client:registerFakers()
+    local function errorhandler(err)
+        print(err)
+        print(debug.traceback())
+    end
 
-    client:initialize {
-        rootUri = rootUri,
-    }
+    ---@async
+    xpcall(lclient.start, errorhandler, lclient, function (client)
+        await.disable()
+        client:registerFakers()
 
-    client:register('textDocument/publishDiagnostics', function (params)
-        results[params.uri] = params.diagnostics
-    end)
+        client:initialize {
+            rootUri = rootUri,
+        }
 
-    if not QUIET then
-        io.write(lang.script('CLI_CHECK_INITING'))
-    end
+        client:register('textDocument/publishDiagnostics', function (params)
+            results[params.uri] = params.diagnostics
+        end)
+
+        if not QUIET then
+            io.write(lang.script('CLI_CHECK_INITING'))
+        end
 
-    provider.updateConfig(rootUri)
+        provider.updateConfig(rootUri)
 
-    ws.awaitReady(rootUri)
+        ws.awaitReady(rootUri)
 
-    local disables = util.arrayToHash(config.get(rootUri, 'Lua.diagnostics.disable'))
-    for name, serverity in pairs(define.DiagnosticDefaultSeverity) do
-        serverity = config.get(rootUri, 'Lua.diagnostics.severity')[name] or serverity
-        if serverity:sub(-1) == '!' then
-            serverity = serverity:sub(1, -2)
-        end
-        if define.DiagnosticSeverity[serverity] > checkLevel then
-            disables[name] = true
+        local disables = util.arrayToHash(config.get(rootUri, 'Lua.diagnostics.disable'))
+        for name, serverity in pairs(define.DiagnosticDefaultSeverity) do
+            serverity = config.get(rootUri, 'Lua.diagnostics.severity')[name] or serverity
+            if serverity:sub(-1) == '!' then
+                serverity = serverity:sub(1, -2)
+            end
+            if define.DiagnosticSeverity[serverity] > checkLevel then
+                disables[name] = true
+            end
         end
-    end
-    config.set(rootUri, 'Lua.diagnostics.disable', util.getTableKeys(disables, true))
+        config.set(rootUri, 'Lua.diagnostics.disable', util.getTableKeys(disables, true))
 
-    -- Downgrade file opened status to Opened for everything to avoid reporting during compilation on files that do not belong to this thread
-    local diagStatus = config.get(rootUri, 'Lua.diagnostics.neededFileStatus')
-    for diag, status in pairs(diagStatus) do
-        if status == 'Any' or status == 'Any!' then
-            diagStatus[diag] = 'Opened!'
+        -- Downgrade file opened status to Opened for everything to avoid reporting during compilation on files that do not belong to this thread
+        local diagStatus = config.get(rootUri, 'Lua.diagnostics.neededFileStatus')
+        for diag, status in pairs(diagStatus) do
+            if status == 'Any' or status == 'Any!' then
+                diagStatus[diag] = 'Opened!'
+            end
         end
-    end
-    for diag, status in pairs(protoDiag.getDefaultStatus()) do
-        if status == 'Any' or status == 'Any!' then
-            diagStatus[diag] = 'Opened!'
+        for diag, status in pairs(protoDiag.getDefaultStatus()) do
+            if status == 'Any' or status == 'Any!' then
+                diagStatus[diag] = 'Opened!'
+            end
         end
-    end
-    config.set(rootUri, 'Lua.diagnostics.neededFileStatus', diagStatus)
-
-    local uris = files.getChildFiles(rootUri)
-    local max  = #uris
-    table.sort(uris)    -- sort file list to ensure the work distribution order across multiple threads
-    for i, uri in ipairs(uris) do
-        if (i % numThreads + 1) == threadId then
-            files.open(uri)
-            diag.doDiagnostic(uri, true)
-            -- Print regularly but always print the last entry to ensure that logs written to files don't look incomplete.
-            if (os.clock() - lastClock > 0.2 or i == #uris) and not QUIET then
-                lastClock = os.clock()
-                client:update()
-                local output = '\x0D'
-                            .. ('>'):rep(math.ceil(i / max * 20))
-                            .. ('='):rep(20 - math.ceil(i / max * 20))
-                            .. ' '
-                            .. ('0'):rep(#tostring(max) - #tostring(i))
-                            .. tostring(i) .. '/' .. tostring(max)
-                io.write(output)
-                local filesWithErrors = 0
-                local errors = 0
-                for _, diags in pairs(results) do
-                    filesWithErrors = filesWithErrors + 1
-                    errors = errors + #diags
+        config.set(rootUri, 'Lua.diagnostics.neededFileStatus', diagStatus)
+
+        local uris = files.getChildFiles(rootUri)
+        local max  = #uris
+        table.sort(uris)    -- sort file list to ensure the work distribution order across multiple threads
+        for i, uri in ipairs(uris) do
+            if (i % numThreads + 1) == threadId then
+                files.open(uri)
+                diag.doDiagnostic(uri, true)
+                -- Print regularly but always print the last entry to ensure that logs written to files don't look incomplete.
+                if (os.clock() - lastClock > 0.2 or i == #uris) and not QUIET then
+                    lastClock = os.clock()
+                    client:update()
+                    local output = '\x0D'
+                                .. ('>'):rep(math.ceil(i / max * 20))
+                                .. ('='):rep(20 - math.ceil(i / max * 20))
+                                .. ' '
+                                .. ('0'):rep(#tostring(max) - #tostring(i))
+                                .. tostring(i) .. '/' .. tostring(max)
+                    io.write(output)
+                    local filesWithErrors = 0
+                    local errors = 0
+                    for _, diags in pairs(results) do
+                        filesWithErrors = filesWithErrors + 1
+                        errors = errors + #diags
+                    end
+                    if errors > 0 then
+                        local errorDetails = ' [' .. lang.script('CLI_CHECK_PROGRESS', errors, filesWithErrors) .. ']'
+                        io.write(errorDetails)
+                    end
+                    io.flush()
                 end
-                if errors > 0 then
-                    local errorDetails = ' [' .. lang.script('CLI_CHECK_PROGRESS', errors, filesWithErrors) .. ']'
-                    io.write(errorDetails)
-                end
-                io.flush()
             end
         end
+        if not QUIET then
+            io.write('\x0D')
+        end
+    end)
+
+    local count = 0
+    for uri, result in pairs(results) do
+        count = count + #result
+        if #result == 0 then
+            results[uri] = nil
+        end
     end
-    if not QUIET then
-        io.write('\x0D')
-    end
-end)
 
-local count = 0
-for uri, result in pairs(results) do
-    count = count + #result
-    if #result == 0 then
-        results[uri] = nil
+    local outpath = CHECK_OUT_PATH
+    if outpath == nil then
+        outpath = LOGPATH .. '/check.json'
     end
-end
+    -- Always write result, even if it's empty to make sure no one accidentally looks at an old output after a successful run.
+    util.saveFile(outpath, jsonb.beautify(results))
 
-local outpath = CHECK_OUT_PATH
-if outpath == nil then
-    outpath = LOGPATH .. '/check.json'
-end
--- Always write result, even if it's empty to make sure no one accidentally looks at an old output after a successful run.
-util.saveFile(outpath, jsonb.beautify(results))
-
-if not QUIET then
-    if count == 0 then
-        print(lang.script('CLI_CHECK_SUCCESS'))
-    else
-        print(lang.script('CLI_CHECK_RESULTS', count, outpath))
+    if not QUIET then
+        if count == 0 then
+            print(lang.script('CLI_CHECK_SUCCESS'))
+        else
+            print(lang.script('CLI_CHECK_RESULTS', count, outpath))
+        end
     end
+    return count == 0 and 0 or 1
 end
+
+return export
diff --git a/script/cli/init.lua b/script/cli/init.lua
index 65e7e102a..37554123e 100644
--- a/script/cli/init.lua
+++ b/script/cli/init.lua
@@ -4,13 +4,13 @@ if _G['VERSION'] then
 end
 
 if _G['CHECK'] then
-    require 'cli.check'
-    os.exit(0, true)
+    local ret = require 'cli.check'.runCLI()
+    os.exit(ret, true)
 end
 
 if _G['CHECK_WORKER'] then
-    require 'cli.check_worker'
-    os.exit(0, true)
+    local ret = require 'cli.check_worker'.runCLI()
+    os.exit(ret or 0, true)
 end
 
 if _G['DOC_UPDATE'] then

From a3dd298664a55be46a2603642559eb91656ba18e Mon Sep 17 00:00:00 2001
From: Andrew Braxton <andrewcbraxton@gmail.com>
Date: Tue, 24 Dec 2024 22:21:01 -0500
Subject: [PATCH 02/34] Send the correct `insertTextFormat`

Per LSP specification, `insertTextFormat` should be `2` for snippets and
`1` for plain text. Using `2` unconditionally can make clients behave in
unexpected ways.

The code calling into `provider.lua` sets `insertTextFormat` to 2 for
all snippets and leaves it nil for other types, so we can reuse that
information in `provider.lua` to make sure we send the correct value.
---
 script/provider/provider.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/script/provider/provider.lua b/script/provider/provider.lua
index af65110a5..08031213e 100644
--- a/script/provider/provider.lua
+++ b/script/provider/provider.lua
@@ -661,7 +661,7 @@ m.register 'textDocument/completion' {
                 sortText         = res.sortText or ('%04d'):format(i),
                 filterText       = res.filterText,
                 insertText       = res.insertText,
-                insertTextFormat = 2,
+                insertTextFormat = res.insertTextFormat or 1,
                 commitCharacters = res.commitCharacters,
                 command          = res.command,
                 textEdit         = res.textEdit and {

From aa02a9b2a84af233fda02680fceb3becb892c3d7 Mon Sep 17 00:00:00 2001
From: Mc-GrowlR <growlr@126.com>
Date: Fri, 27 Dec 2024 23:42:02 +0800
Subject: [PATCH 03/34] Fix `--configpath` not load

---
 script/cli/doc/init.lua      | 24 +++++++++++++++++-------
 script/lclient.lua           |  4 ++--
 script/provider/provider.lua |  2 +-
 3 files changed, 20 insertions(+), 10 deletions(-)

diff --git a/script/cli/doc/init.lua b/script/cli/doc/init.lua
index 483ea21f9..86cae3e07 100644
--- a/script/cli/doc/init.lua
+++ b/script/cli/doc/init.lua
@@ -41,7 +41,7 @@ local function getPathDocUpdate()
                     return section.DOC
                 end
             end
-    end)
+        end)
     if ok then
         local doc_json_dir = doc_json_path:string():gsub('/doc.json', '')
         return doc_json_dir, doc_path
@@ -131,7 +131,7 @@ local function injectBuildScript()
         },
         {__index = _G}))
     if err or not data then
-       error(err, 0)
+        error(err, 0)
     end
     data()
     return module
@@ -193,6 +193,15 @@ function doc.runCLI()
 
     print('root uri = ' .. rootUri)
 
+    --- If '--configpath' is specified, get the folder path of the '.luarc.doc.josn' configuration file (without the file name)
+    --- 如果指定了'--configpath'，则获取`.luarc.doc.josn` 配置文件的文件夹路径(不包含文件名）
+    --- This option is passed into the callback function of the initialized method in provide.
+    --- 该选项会被传入到`provide`中的`initialized`方法的回调函数中
+    local luarcParentUri
+    if CONFIGPATH then
+        luarcParentUri = furi.encode(fs.absolute(fs.path(CONFIGPATH)):parent_path():string())
+    end
+
     util.enableCloseFunction()
 
     local lastClock = os.clock()
@@ -203,6 +212,7 @@ function doc.runCLI()
 
         client:initialize {
             rootUri = rootUri,
+            luarcParentUri = luarcParentUri
         }
         io.write(lang.script('CLI_DOC_INITING'))
 
@@ -222,11 +232,11 @@ function doc.runCLI()
             if os.clock() - lastClock > 0.2 then
                 lastClock = os.clock()
                 local output = '\x0D'
-                            .. ('>'):rep(math.ceil(i / max * 20))
-                            .. ('='):rep(20 - math.ceil(i / max * 20))
-                            .. ' '
-                            .. ('0'):rep(#tostring(max) - #tostring(i))
-                            .. tostring(i) .. '/' .. tostring(max)
+                    .. ('>'):rep(math.ceil(i / max * 20))
+                    .. ('='):rep(20 - math.ceil(i / max * 20))
+                    .. ' '
+                    .. ('0'):rep(#tostring(max) - #tostring(i))
+                    .. tostring(i) .. '/' .. tostring(max)
                 io.write(output)
             end
         end)
diff --git a/script/lclient.lua b/script/lclient.lua
index 0909e9321..513e311cf 100644
--- a/script/lclient.lua
+++ b/script/lclient.lua
@@ -77,7 +77,7 @@ local defaultClientOptions = {
                         valueSet = {
                             define.DiagnosticTag.Unnecessary,
                             define.DiagnosticTag.Deprecated,
-                         },
+                        },
                     },
                 },
             },
@@ -92,7 +92,7 @@ local defaultClientOptions = {
 function mt:initialize(params)
     local initParams = util.tableMerge(params or {}, defaultClientOptions)
     self:awaitRequest('initialize', initParams)
-    self:notify('initialized')
+    self:notify('initialized', initParams)
 end
 
 function mt:reportHangs()
diff --git a/script/provider/provider.lua b/script/provider/provider.lua
index 08031213e..3f79f4ea5 100644
--- a/script/provider/provider.lua
+++ b/script/provider/provider.lua
@@ -141,7 +141,7 @@ m.register 'initialized'{
     ---@async
     function (_params)
         local _ <close> = progress.create(workspace.getFirstScope().uri, lang.script.WINDOW_INITIALIZING, 0.5)
-        m.updateConfig()
+        m.updateConfig((_params or {}).luarcParentUri)
         local registrations = {}
 
         if client.getAbility 'workspace.didChangeConfiguration.dynamicRegistration' then

From 6ac41ac77d84b238aa44f1615dc6393a955b7925 Mon Sep 17 00:00:00 2001
From: Mc-GrowlR <growlr@126.com>
Date: Sat, 28 Dec 2024 00:15:25 +0800
Subject: [PATCH 04/34] fix `--configpath` not load

---
 changelog.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/changelog.md b/changelog.md
index bdea8e280..f948369b6 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 <!-- Add all new changes here. They will be moved under a version at release -->
+* `FIX` Fixed the error that the configuration file pointed to by the `--configpath` option was not read and loaded.
 
 ## 3.13.5
 `2024-12-20`

From 0703a80c0d080ef3d668c6dcfc5ca3bdaec89c20 Mon Sep 17 00:00:00 2001
From: Mc-GrowlR <growlr@126.com>
Date: Sat, 28 Dec 2024 12:38:09 +0800
Subject: [PATCH 05/34] Fixed some minor issues

---
 script/cli/doc/init.lua      | 6 +++---
 script/provider/provider.lua | 3 ++-
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/script/cli/doc/init.lua b/script/cli/doc/init.lua
index 86cae3e07..3492b3d9c 100644
--- a/script/cli/doc/init.lua
+++ b/script/cli/doc/init.lua
@@ -193,8 +193,8 @@ function doc.runCLI()
 
     print('root uri = ' .. rootUri)
 
-    --- If '--configpath' is specified, get the folder path of the '.luarc.doc.josn' configuration file (without the file name)
-    --- 如果指定了'--configpath'，则获取`.luarc.doc.josn` 配置文件的文件夹路径(不包含文件名）
+    --- If '--configpath' is specified, get the folder path of the '.luarc.doc.json' configuration file (without the file name)
+    --- 如果指定了'--configpath'，则获取`.luarc.doc.json` 配置文件的文件夹路径(不包含文件名）
     --- This option is passed into the callback function of the initialized method in provide.
     --- 该选项会被传入到`provide`中的`initialized`方法的回调函数中
     local luarcParentUri
@@ -212,7 +212,7 @@ function doc.runCLI()
 
         client:initialize {
             rootUri = rootUri,
-            luarcParentUri = luarcParentUri
+            luarcParentUri = luarcParentUri,
         }
         io.write(lang.script('CLI_DOC_INITING'))
 
diff --git a/script/provider/provider.lua b/script/provider/provider.lua
index 3f79f4ea5..c1e9a0d20 100644
--- a/script/provider/provider.lua
+++ b/script/provider/provider.lua
@@ -141,7 +141,8 @@ m.register 'initialized'{
     ---@async
     function (_params)
         local _ <close> = progress.create(workspace.getFirstScope().uri, lang.script.WINDOW_INITIALIZING, 0.5)
-        m.updateConfig((_params or {}).luarcParentUri)
+        --- 传递`.luarc.doc.json`文件所在的文件夹路径
+        m.updateConfig(_params and _params.luarcParentUri)
         local registrations = {}
 
         if client.getAbility 'workspace.didChangeConfiguration.dynamicRegistration' then

From 5b360d5a8090db3b74dd2b7a1c70931a1ee62c0a Mon Sep 17 00:00:00 2001
From: fesily <fesil@foxmail.com>
Date: Sat, 28 Dec 2024 14:17:46 +0800
Subject: [PATCH 06/34] unittest: support test specify unit test

---
 changelog.md |  1 +
 test.lua     | 13 +++++++++++++
 2 files changed, 14 insertions(+)

diff --git a/changelog.md b/changelog.md
index bdea8e280..4909c58c1 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 <!-- Add all new changes here. They will be moved under a version at release -->
+* `NEW` Test CLI: `--name=<testname>` `-n=<testname>`: run specify unit test
 
 ## 3.13.5
 `2024-12-20`
diff --git a/test.lua b/test.lua
index 1ede0b3ed..bd1d8cb06 100644
--- a/test.lua
+++ b/test.lua
@@ -10,6 +10,15 @@ DEVELOP = true
 --TRACE = true
 LOGPATH  = LOGPATH  or (ROOT:string() .. '/log')
 METAPATH = METAPATH or (ROOT:string() .. '/meta')
+TARGET_TEST_NAME = nil
+
+if arg then
+   for _, v in pairs(arg) do
+       if v:sub(1, 3) == "-n=" or v:sub(1, 7) == "--name=" then
+            TARGET_TEST_NAME = v:sub(v:find('=') + 1)
+       end
+   end
+end
 
 collectgarbage 'generational'
 
@@ -34,7 +43,11 @@ local function loadAllLibs()
     assert(require 'lpeglabel')
 end
 
+---@param name string
 local function test(name)
+    if TARGET_TEST_NAME and not name:match(TARGET_TEST_NAME) then
+        return
+    end
     local clock = os.clock()
     print(('测试[%s]...'):format(name))
     local originRequire = require

From 4a78e36b29becd1b04c2b4bb5ad1798d30ec7f90 Mon Sep 17 00:00:00 2001
From: Mc-GrowlR <growlr@126.com>
Date: Sat, 28 Dec 2024 20:23:19 +0800
Subject: [PATCH 07/34] delete `_`

---
 script/provider/provider.lua | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/script/provider/provider.lua b/script/provider/provider.lua
index c1e9a0d20..8b988059e 100644
--- a/script/provider/provider.lua
+++ b/script/provider/provider.lua
@@ -139,10 +139,10 @@ m.register 'initialize' {
 
 m.register 'initialized'{
     ---@async
-    function (_params)
+    function (params)
         local _ <close> = progress.create(workspace.getFirstScope().uri, lang.script.WINDOW_INITIALIZING, 0.5)
         --- 传递`.luarc.doc.json`文件所在的文件夹路径
-        m.updateConfig(_params and _params.luarcParentUri)
+        m.updateConfig(params and params.luarcParentUri)
         local registrations = {}
 
         if client.getAbility 'workspace.didChangeConfiguration.dynamicRegistration' then

From 4cfd0304f87e29806646bf2b24cf1e1c83ae69d1 Mon Sep 17 00:00:00 2001
From: Shararvev <shararvev@gmail.com>
Date: Sun, 29 Dec 2024 20:17:08 +0500
Subject: [PATCH 08/34] #3014. Parser now considers . and * as Symbol. Improve
 parseCodePattern to support "`T`.*-", prevent crash with "`T``T`" and when
 tokens >= 8, fix wrong getStart of result.

---
 script/parser/luadoc.lua | 28 ++++++++++++++++++++--------
 1 file changed, 20 insertions(+), 8 deletions(-)

diff --git a/script/parser/luadoc.lua b/script/parser/luadoc.lua
index d108cebc2..a51865889 100644
--- a/script/parser/luadoc.lua
+++ b/script/parser/luadoc.lua
@@ -54,7 +54,7 @@ EChar               <-  'a' -> ea
                     /   ([0-9] [0-9]? [0-9]?) -> Char10
                     /   ('u{' {X16*} '}')    -> CharUtf8
 Symbol              <-  ({} {
-                            [:|,;<>()?+#{}]
+                            [:|,;<>()?+#{}.*]
                         /   '[]'
                         /   '...'
                         /   '['
@@ -744,30 +744,42 @@ local function parseCodePattern(parent)
     local codeOffset
     local finishOffset
     local content
-    for i = 2, 8 do
+    local i = 2
+    while true do
         local next, nextContent = peekToken(i)
         if not next or TokenFinishs[Ci+i-1] + 1 ~= TokenStarts[Ci+i] then
             if codeOffset then
-                finishOffset = i
+                finishOffset = i-1
                 break
             end
             ---不连续的name，无效的
             return nil
         end
-        if next == 'code' then
-            if codeOffset and content ~= nextContent then
+        if next == 'name' then
+            pattern = pattern .. nextContent
+        elseif next == 'code' then
+            if codeOffset then
                 -- 暂时不支持多generic
                 return nil
             end
             codeOffset = i
             pattern = pattern .. "%s"
             content = nextContent
-        elseif next ~= 'name' then
-            return nil
+        elseif codeOffset then
+            -- should be match with Parser "name" mask
+            if next == 'integer' then
+                pattern = pattern .. nextContent
+            elseif next == 'symbol' and (nextContent == '.' or nextContent == '*' or nextContent == '-') then
+                pattern = pattern .. nextContent
+            else
+                return nil
+            end
         else
-            pattern = pattern .. nextContent
+            return nil
         end
+        i = i + 1
     end
+    nextToken()
     local start = getStart()
     for _ = 2, finishOffset do
         nextToken()

From f4a69bcfaa32bf7f51332f97ed84fd0b3b045fa0 Mon Sep 17 00:00:00 2001
From: Shararvev <shararvev@gmail.com>
Date: Sun, 29 Dec 2024 21:07:08 +0500
Subject: [PATCH 09/34] #3014. Replace parseCode with parseCodePattern to
 support "`T`.*-" without name token before code. Added tests.

---
 script/parser/luadoc.lua   | 39 ++++++++++-------------
 test/definition/luadoc.lua | 64 +++++++++++++++++++++++++++++++++++---
 2 files changed, 75 insertions(+), 28 deletions(-)

diff --git a/script/parser/luadoc.lua b/script/parser/luadoc.lua
index a51865889..250cf304a 100644
--- a/script/parser/luadoc.lua
+++ b/script/parser/luadoc.lua
@@ -720,32 +720,22 @@ local function parseString(parent)
     return str
 end
 
-local function parseCode(parent)
-    local tp, content = peekToken()
-    if not tp or tp ~= 'code' then
-        return nil
-    end
-    nextToken()
-    local code = {
-        type   = 'doc.type.code',
-        start  = getStart(),
-        finish = getFinish(),
-        parent = parent,
-        [1]    = content,
-    }
-    return code
-end
-
 local function parseCodePattern(parent)
     local tp, pattern = peekToken()
-    if not tp or tp ~= 'name' then
+    if not tp or (tp ~= 'name' and tp ~= 'code') then
         return nil
     end
     local codeOffset
     local finishOffset
     local content
-    local i = 2
+    local i = 1
+    if tp == 'code' then
+        codeOffset = i
+        content = pattern
+        pattern = '%s'
+    end
     while true do
+        i = i + 1
         local next, nextContent = peekToken(i)
         if not next or TokenFinishs[Ci+i-1] + 1 ~= TokenStarts[Ci+i] then
             if codeOffset then
@@ -763,7 +753,7 @@ local function parseCodePattern(parent)
                 return nil
             end
             codeOffset = i
-            pattern = pattern .. "%s"
+            pattern = pattern .. '%s'
             content = nextContent
         elseif codeOffset then
             -- should be match with Parser "name" mask
@@ -777,12 +767,16 @@ local function parseCodePattern(parent)
         else
             return nil
         end
-        i = i + 1
     end
     nextToken()
     local start = getStart()
-    for _ = 2, finishOffset do
-        nextToken()
+    if finishOffset == 1 then
+        -- code only, no pattern
+        pattern = nil
+    else
+        for _ = 2, finishOffset do
+            nextToken()
+        end
     end
     local code = {
         type   = 'doc.type.code',
@@ -846,7 +840,6 @@ function parseTypeUnit(parent)
                 or parseTable(parent)
                 or parseTuple(parent)
                 or parseString(parent)
-                or parseCode(parent)
                 or parseInteger(parent)
                 or parseBoolean(parent)
                 or parseParen(parent)
diff --git a/test/definition/luadoc.lua b/test/definition/luadoc.lua
index 7a460593a..704c9f788 100644
--- a/test/definition/luadoc.lua
+++ b/test/definition/luadoc.lua
@@ -239,7 +239,7 @@ TEST [[
 AAAA = {};
 
 function AAAA:<!SSDF!>()
-    
+
 end
 
 AAAA.a.<?SSDF?>
@@ -304,6 +304,19 @@ local v1 = Generic(Foo)
 print(v1.<?bar1?>)
 ]]
 
+TEST [[
+---@class Foo
+local Foo = {}
+function Foo:<!bar1!>() end
+
+---@generic T
+---@param arg1 `T`
+---@return T
+function Generic(arg1) print(arg1) end
+
+local v1 = Generic("Foo")
+print(v1.<?bar1?>)
+]]
 
 TEST [[
 ---@class n.Foo
@@ -320,12 +333,12 @@ print(v1.<?bar1?>)
 ]]
 
 TEST [[
----@class Foo
+---@class n.Foo
 local Foo = {}
 function Foo:<!bar1!>() end
 
 ---@generic T
----@param arg1 `T`
+---@param arg1 n.`T`
 ---@return T
 function Generic(arg1) print(arg1) end
 
@@ -333,14 +346,55 @@ local v1 = Generic("Foo")
 print(v1.<?bar1?>)
 ]]
 
+TEST [[
+---@class Foo*
+local Foo = {}
+function Foo:bar1() end
+
+---@generic T
+---@param arg1 `T`*
+---@return T
+function Generic(arg1) print(arg1) end
+
+local v1 = Generic(Foo)
+print(v1.<?bar1?>)
+]]
 
 TEST [[
----@class n.Foo
+---@class Foo*
 local Foo = {}
 function Foo:<!bar1!>() end
 
 ---@generic T
----@param arg1 n.`T`
+---@param arg1 `T`*
+---@return T
+function Generic(arg1) print(arg1) end
+
+local v1 = Generic("Foo")
+print(v1.<?bar1?>)
+]]
+
+TEST [[
+---@class n.Foo*
+local Foo = {}
+function Foo:bar1() end
+
+---@generic T
+---@param arg1 n.`T`*
+---@return T
+function Generic(arg1) print(arg1) end
+
+local v1 = Generic(Foo)
+print(v1.<?bar1?>)
+]]
+
+TEST [[
+---@class n.Foo*
+local Foo = {}
+function Foo:<!bar1!>() end
+
+---@generic T
+---@param arg1 n.`T`*
 ---@return T
 function Generic(arg1) print(arg1) end
 

From e66a8abad4509cef8cdd955c6ca312b535a5568f Mon Sep 17 00:00:00 2001
From: Shararvev <shararvev@gmail.com>
Date: Sun, 29 Dec 2024 22:32:20 +0500
Subject: [PATCH 10/34] #3014. Fixed parsing of `...` Symbol but still
 separated 2 dots `..`, added changelog.

---
 changelog.md               | 9 +++++++++
 script/parser/luadoc.lua   | 3 ++-
 test/definition/luadoc.lua | 8 ++++----
 3 files changed, 15 insertions(+), 5 deletions(-)

diff --git a/changelog.md b/changelog.md
index bdea8e280..2e4e7801b 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,15 @@
 
 ## Unreleased
 <!-- Add all new changes here. They will be moved under a version at release -->
+* `CHG` [#3014] Generic pattern now supports definition after capture
+  ```lua
+  ---@generic T
+  ---@param t `T`.Cat
+  ---@return T
+  local function f(t) end
+
+  local t = f('Smile') --> t is `Smile.Cat`
+  ```
 
 ## 3.13.5
 `2024-12-20`
diff --git a/script/parser/luadoc.lua b/script/parser/luadoc.lua
index 250cf304a..3fec5c182 100644
--- a/script/parser/luadoc.lua
+++ b/script/parser/luadoc.lua
@@ -54,12 +54,13 @@ EChar               <-  'a' -> ea
                     /   ([0-9] [0-9]? [0-9]?) -> Char10
                     /   ('u{' {X16*} '}')    -> CharUtf8
 Symbol              <-  ({} {
-                            [:|,;<>()?+#{}.*]
+                            [:|,;<>()?+#{}*]
                         /   '[]'
                         /   '...'
                         /   '['
                         /   ']'
                         /   '-' !'-'
+                        /   '.' !'..'
                         } {})
                     ->  Symbol
 ]], {
diff --git a/test/definition/luadoc.lua b/test/definition/luadoc.lua
index 704c9f788..d6e0d467c 100644
--- a/test/definition/luadoc.lua
+++ b/test/definition/luadoc.lua
@@ -375,12 +375,12 @@ print(v1.<?bar1?>)
 ]]
 
 TEST [[
----@class n.Foo*
+---@class n.Foo.2
 local Foo = {}
 function Foo:bar1() end
 
 ---@generic T
----@param arg1 n.`T`*
+---@param arg1 n.`T`.2
 ---@return T
 function Generic(arg1) print(arg1) end
 
@@ -389,12 +389,12 @@ print(v1.<?bar1?>)
 ]]
 
 TEST [[
----@class n.Foo*
+---@class n.Foo.2
 local Foo = {}
 function Foo:<!bar1!>() end
 
 ---@generic T
----@param arg1 n.`T`*
+---@param arg1 n.`T`.2
 ---@return T
 function Generic(arg1) print(arg1) end
 

From 3e0d1df77740c768cea1d48dd826e4cce9cb3461 Mon Sep 17 00:00:00 2001
From: Andrew Braxton <andrewcbraxton@gmail.com>
Date: Thu, 2 Jan 2025 23:23:45 -0500
Subject: [PATCH 11/34] Mark function completion items as snippets

The PR #3005 assumed that all snippets were setting `insertTextFormat`
to 2 and leaving plaintext completions nil, but that was not the case.
Function completions need to be marked as snippets too.

fixes #3021
---
 script/core/completion/completion.lua | 1 +
 1 file changed, 1 insertion(+)

diff --git a/script/core/completion/completion.lua b/script/core/completion/completion.lua
index 1ec295242..f751e53d5 100644
--- a/script/core/completion/completion.lua
+++ b/script/core/completion/completion.lua
@@ -1351,6 +1351,7 @@ local function insertEnum(state, pos, src, enums, isInArray, mark)
             description = description,
             kind        = define.CompletionItemKind.Function,
             insertText  = insertText,
+            insertTextFormat = 2,
         }
     elseif src.type == 'doc.enum' then
         ---@cast src parser.object

From 096274d433bff9f72b8331da34a48877053315a1 Mon Sep 17 00:00:00 2001
From: Tom Lau <tomandfatboy@gmail.com>
Date: Fri, 3 Jan 2025 20:33:09 +0800
Subject: [PATCH 12/34] feat: basic partial class support

a class marked with partial will not check missing inherited fields
---
 script/core/diagnostics/missing-fields.lua | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

diff --git a/script/core/diagnostics/missing-fields.lua b/script/core/diagnostics/missing-fields.lua
index 2459f7cf8..ed9bf1a6a 100644
--- a/script/core/diagnostics/missing-fields.lua
+++ b/script/core/diagnostics/missing-fields.lua
@@ -26,6 +26,18 @@ return function (uri, callback)
                 local className = def.class[1]
                 if not sortedDefs[className] then
                     sortedDefs[className] = {}
+                    -- check if this class is a `partial` class
+                    -- a partial class will not check missing inherited fields
+                    local class = vm.getGlobal('type', className)
+                    ---@cast class -nil
+                    for _, set in ipairs(class:getSets(uri)) do
+                        if set.type == 'doc.class'
+                        and vm.docHasAttr(set, 'partial')
+                        then
+                            sortedDefs[className].isPartial = true
+                            break
+                        end
+                    end
                 end
                 local samedefs = sortedDefs[className]
                 samedefs[#samedefs+1] = def
@@ -41,8 +53,8 @@ return function (uri, callback)
         for className, samedefs in pairs(sortedDefs) do
             local missedKeys = {}
             for _, def in ipairs(samedefs) do
-                local fields = vm.getFields(def)
-                if #fields == 0 then
+                local fields = samedefs.isPartial and def.fields or vm.getFields(def)
+                if not fields or #fields == 0 then
                     goto continue
                 end
 
@@ -78,6 +90,12 @@ return function (uri, callback)
                     end
                 end
                 ::continue::
+
+                if not samedefs.isPartial then
+                    -- if not partial class, then all fields in this class have already been checked
+                    -- because in the above uses `vm.getFields` to get all fields
+                    break
+                end
             end
 
             if #missedKeys == 0 then

From 624e29371285d687fe3989f71b67a7777c3f69b7 Mon Sep 17 00:00:00 2001
From: Tom Lau <tomandfatboy@gmail.com>
Date: Fri, 3 Jan 2025 20:40:25 +0800
Subject: [PATCH 13/34] test: add tests

---
 test/diagnostics/missing-fields.lua | 34 +++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/test/diagnostics/missing-fields.lua b/test/diagnostics/missing-fields.lua
index 4b460531c..66abfe162 100644
--- a/test/diagnostics/missing-fields.lua
+++ b/test/diagnostics/missing-fields.lua
@@ -462,4 +462,38 @@ local function f(b) end
 f <!{y = 1}!>
 ]]
 
+-- partial class
+
+TEST[[
+---@class A
+---@field x number
+
+---@class (partial) B: A
+
+---@type B
+local t = {}
+]]
+
+TEST[[
+---@class A
+---@field x number
+
+---@class (partial) B: A
+---@field y number
+
+---@type B
+local t = <!{}!>
+]]
+
+TEST[[
+---@class A
+---@field x number
+
+---@class (partial) B: A
+---@field y number
+
+---@type B
+local t = {y = 1}
+]]
+
 --

From c2b7dc2667387a1f3dd73ff0892522788acce920 Mon Sep 17 00:00:00 2001
From: Tom Lau <tomandfatboy@gmail.com>
Date: Fri, 3 Jan 2025 21:03:54 +0800
Subject: [PATCH 14/34] doc: update changelog

---
 changelog.md | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/changelog.md b/changelog.md
index 8b5f93f76..7008e34e7 100644
--- a/changelog.md
+++ b/changelog.md
@@ -13,6 +13,20 @@
   ```
 * `NEW` Test CLI: `--name=<testname>` `-n=<testname>`: run specify unit test
 * `FIX` Fixed the error that the configuration file pointed to by the `--configpath` option was not read and loaded.
+* `NEW` `---@class` supports attribute `partial`, which will not check missing inherited fields [#3023](https://github.com/LuaLS/lua-language-server/issues/3023)
+  ```lua
+  ---@class Config
+  ---@field a number
+
+  ---@class (partial) Config.P: Config
+  ---@field b number
+
+  ---@type Config.P[]
+  local cfgs = {}
+  cfgs[1] = { b = 1 } -- no warning
+  cfgs[2] = {}        -- only warns missing `b`
+  ```
+  This enables the previous missing field check behavior before [#2970](https://github.com/LuaLS/lua-language-server/issues/2970)
 
 ## 3.13.5
 `2024-12-20`

From 174a333b2b505c8d83413c30f546daf6a269dfb3 Mon Sep 17 00:00:00 2001
From: Shararvev <shararvev@gmail.com>
Date: Thu, 2 Jan 2025 17:43:21 +0500
Subject: [PATCH 15/34] #3014, #3015 Generic pattern supports optional, union,
 array and comment without a space. Fixed regression.

---
 changelog.md               |  8 ++++----
 script/parser/luadoc.lua   | 35 +++++++++++++++++------------------
 test/definition/luadoc.lua | 34 +++++++++++++++++++++++++++++++---
 3 files changed, 52 insertions(+), 25 deletions(-)

diff --git a/changelog.md b/changelog.md
index 8b5f93f76..2650369b1 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,14 +2,14 @@
 
 ## Unreleased
 <!-- Add all new changes here. They will be moved under a version at release -->
-* `CHG` [#3014] Generic pattern now supports definition after capture
+* `CHG` [#3014] Generic pattern now supports definition after capture and optional, union, array
   ```lua
   ---@generic T
-  ---@param t `T`.Cat
-  ---@return T
+  ---@param t `T`.Cat?
+  ---@return T?
   local function f(t) end
 
-  local t = f('Smile') --> t is `Smile.Cat`
+  local t = f('Smile') --> t is `(Smile.Cat)?`
   ```
 * `NEW` Test CLI: `--name=<testname>` `-n=<testname>`: run specify unit test
 * `FIX` Fixed the error that the configuration file pointed to by the `--configpath` option was not read and loaded.
diff --git a/script/parser/luadoc.lua b/script/parser/luadoc.lua
index 3fec5c182..35f121748 100644
--- a/script/parser/luadoc.lua
+++ b/script/parser/luadoc.lua
@@ -72,7 +72,7 @@ Symbol              <-  ({} {
     er = '\r',
     et = '\t',
     ev = '\v',
-    name = (m.R('az', 'AZ', '09', '\x80\xff') + m.S('_')) * (m.R('az', 'AZ', '__', '09', '\x80\xff') + m.S('_.*-'))^0,
+    name = (m.R('az', 'AZ', '09', '\x80\xff') + m.S('_')) * (m.R('az', 'AZ', '09', '\x80\xff') + m.S('_.*-'))^0,
     Char10 = function (char)
         ---@type integer?
         char = tonumber(char)
@@ -727,7 +727,6 @@ local function parseCodePattern(parent)
         return nil
     end
     local codeOffset
-    local finishOffset
     local content
     local i = 1
     if tp == 'code' then
@@ -736,41 +735,41 @@ local function parseCodePattern(parent)
         pattern = '%s'
     end
     while true do
-        i = i + 1
-        local next, nextContent = peekToken(i)
-        if not next or TokenFinishs[Ci+i-1] + 1 ~= TokenStarts[Ci+i] then
-            if codeOffset then
-                finishOffset = i-1
-                break
-            end
+        i = i+1
+        local nextTp, nextContent = peekToken(i)
+        if not nextTp or TokenFinishs[Ci+i-1] + 1 ~= TokenStarts[Ci+i] then
             ---不连续的name，无效的
-            return nil
+            break
         end
-        if next == 'name' then
+        if nextTp == 'name' then
             pattern = pattern .. nextContent
-        elseif next == 'code' then
+        elseif nextTp == 'code' then
             if codeOffset then
                 -- 暂时不支持多generic
-                return nil
+                break
             end
             codeOffset = i
             pattern = pattern .. '%s'
             content = nextContent
         elseif codeOffset then
             -- should be match with Parser "name" mask
-            if next == 'integer' then
+            if nextTp == 'integer' then
                 pattern = pattern .. nextContent
-            elseif next == 'symbol' and (nextContent == '.' or nextContent == '*' or nextContent == '-') then
+            elseif nextTp == 'symbol' and (nextContent == '.' or nextContent == '*' or nextContent == '-') then
                 pattern = pattern .. nextContent
             else
-                return nil
+                break
             end
         else
-            return nil
+            break
         end
     end
+    if not codeOffset then
+        return nil
+    end
     nextToken()
     local start = getStart()
+    local finishOffset = i-1
     if finishOffset == 1 then
         -- code only, no pattern
         pattern = nil
@@ -932,7 +931,7 @@ function parseType(parent)
     local function pushResume()
         local comments
         for i = 0, 100 do
-            local nextComm = NextComment(i,'peek')
+            local nextComm = NextComment(i, true)
             if not nextComm then
                 return false
             end
diff --git a/test/definition/luadoc.lua b/test/definition/luadoc.lua
index d6e0d467c..e7c2be22d 100644
--- a/test/definition/luadoc.lua
+++ b/test/definition/luadoc.lua
@@ -239,7 +239,7 @@ TEST [[
 AAAA = {};
 
 function AAAA:<!SSDF!>()
-
+    
 end
 
 AAAA.a.<?SSDF?>
@@ -352,7 +352,7 @@ local Foo = {}
 function Foo:bar1() end
 
 ---@generic T
----@param arg1 `T`*
+---@param arg1 `T`*?
 ---@return T
 function Generic(arg1) print(arg1) end
 
@@ -366,7 +366,7 @@ local Foo = {}
 function Foo:<!bar1!>() end
 
 ---@generic T
----@param arg1 `T`*
+---@param arg1 `T`*?
 ---@return T
 function Generic(arg1) print(arg1) end
 
@@ -402,6 +402,34 @@ local v1 = Generic("Foo")
 print(v1.<?bar1?>)
 ]]
 
+TEST [[
+---@class n-Foo-2
+local Foo = {}
+function Foo:bar1() end
+
+---@generic T
+---@param arg1 n-`T`-2[]
+---@return T
+function Generic(arg1) print(arg1) end
+
+local v1 = Generic({Foo})
+print(v1.<?bar1?>)
+]]
+
+TEST [[
+---@class n-Foo-2
+local Foo = {}
+function Foo:<!bar1!>() end
+
+---@generic T
+---@param arg1 n-`T`-2[]
+---@return T
+function Generic(arg1) print(arg1) end
+
+local v1 = Generic({"Foo"})
+print(v1.<?bar1?>)
+]]
+
 TEST [[
 ---@class A
 local t

From 5f0efc4da14c8cb324056e092c87f07f60172115 Mon Sep 17 00:00:00 2001
From: Shararvev <shararvev@gmail.com>
Date: Sun, 5 Jan 2025 05:32:12 +0500
Subject: [PATCH 16/34] Generic return can be optional.

---
 changelog.md                        |  1 +
 script/vm/generic.lua               |  3 +++
 test/diagnostics/missing-return.lua | 18 ++++++++++++++++++
 test/type_inference/common.lua      | 26 +++++++++++++++++++++++++-
 4 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/changelog.md b/changelog.md
index 2650369b1..5d8c483c9 100644
--- a/changelog.md
+++ b/changelog.md
@@ -13,6 +13,7 @@
   ```
 * `NEW` Test CLI: `--name=<testname>` `-n=<testname>`: run specify unit test
 * `FIX` Fixed the error that the configuration file pointed to by the `--configpath` option was not read and loaded.
+* `FIX` Generic return can be optional.
 
 ## 3.13.5
 `2024-12-20`
diff --git a/script/vm/generic.lua b/script/vm/generic.lua
index ed832d9bd..f1eaaf99d 100644
--- a/script/vm/generic.lua
+++ b/script/vm/generic.lua
@@ -136,6 +136,9 @@ function mt:resolve(uri, args)
             end
         end
     end
+    if protoNode:isOptional() then
+        result:addOptional()
+    end
     return result
 end
 
diff --git a/test/diagnostics/missing-return.lua b/test/diagnostics/missing-return.lua
index b8c1e7d37..33b50b16f 100644
--- a/test/diagnostics/missing-return.lua
+++ b/test/diagnostics/missing-return.lua
@@ -156,3 +156,21 @@ function F()
 
 end
 ]]
+
+TEST [[
+---@generic T
+---@param t T
+---@return T
+function F(t)
+	return t
+end
+]]
+
+TEST [[
+---@generic T
+---@param t T
+---@return T?
+function F(t)
+
+end
+]]
diff --git a/test/type_inference/common.lua b/test/type_inference/common.lua
index 4140e2b01..5f0fd81a6 100644
--- a/test/type_inference/common.lua
+++ b/test/type_inference/common.lua
@@ -359,8 +359,32 @@ end
 local _, _, _, <?b?>, _ = x(nil, true, 1, 'yy')
 ]]
 
+TEST 'nil' [[
+local <?k?>, <?v?> = next()
+local <?k?>, <?v?> = next({})
+]]
+
+TEST 'integer?' [[
+local <?k?>, <?v?> = next({1})
+]]
+
+TEST 'integer?' [[
+local a
+local <?k?>, v = next({a})
+]]
+
+-- probably explicit unknown generic should be unknown
+TEST 'nil' [[
+local a
+local k, <?v?> = next({a})
+]]
+
 TEST 'unknown' [[
-local <?x?> = next()
+---@generic T
+---@param t T?
+---@return T
+local function F(t) end
+local <?x?> = F()
 ]]
 
 TEST 'unknown' [[

From 9ba27a6edd0ec0c73c3d59b3b1b028a1a46138c6 Mon Sep 17 00:00:00 2001
From: Shararvev <shararvev@gmail.com>
Date: Sun, 5 Jan 2025 03:36:45 +0500
Subject: [PATCH 17/34] #3028. Fix calculating the comment over `doc` not
 `text` in buildLuaDoc, sync with semantic logic. Fix crash 'comment.clong'
 type for /*, fix typo docGeneric.

---
 changelog.md                    |  1 +
 script/core/semantic-tokens.lua | 16 ++++++++--------
 script/parser/compile.lua       |  2 +-
 script/parser/luadoc.lua        | 24 ++++++++++++------------
 script/vm/sign.lua              |  6 +++---
 5 files changed, 25 insertions(+), 24 deletions(-)

diff --git a/changelog.md b/changelog.md
index 8b5f93f76..593038aed 100644
--- a/changelog.md
+++ b/changelog.md
@@ -13,6 +13,7 @@
   ```
 * `NEW` Test CLI: `--name=<testname>` `-n=<testname>`: run specify unit test
 * `FIX` Fixed the error that the configuration file pointed to by the `--configpath` option was not read and loaded.
+* `FIX` Fixed the comment calculating in docs `---@param a string?Comment` - now its `Comment` instead of `omment`.
 
 ## 3.13.5
 `2024-12-20`
diff --git a/script/core/semantic-tokens.lua b/script/core/semantic-tokens.lua
index 25df6b3c1..ad4162e6f 100644
--- a/script/core/semantic-tokens.lua
+++ b/script/core/semantic-tokens.lua
@@ -902,23 +902,23 @@ return function (uri, start, finish)
             return
         end
         if start <= comm.start and comm.finish <= finish then
+            -- the same logic as in buildLuaDoc
             local headPos = (comm.type == 'comment.short' and comm.text:match '^%-%s*[@|]()')
                          or (comm.type == 'comment.long'  and comm.text:match '^@()')
             if headPos then
-                local atPos
-                if comm.type == 'comment.short' then
-                    atPos = headPos + 2
-                else
-                    atPos = headPos + #comm.mark
+                -- absolute position of `@` symbol
+                local startOffset = comm.start + headPos
+                if comm.type == 'comment.long' then
+                    startOffset = comm.start + headPos + #comm.mark - 2
                 end
                 results[#results+1] = {
                     start  = comm.start,
-                    finish = comm.start + atPos - 2,
+                    finish = startOffset,
                     type   = define.TokenTypes.comment,
                 }
                 results[#results+1] = {
-                    start      = comm.start + atPos - 2,
-                    finish     = comm.start + atPos - 1 + #comm.text:match('%S*', headPos),
+                    start      = startOffset,
+                    finish     = startOffset + #comm.text:match('%S*', headPos) + 1,
                     type       = define.TokenTypes.keyword,
                     modifieres = define.TokenModifiers.documentation,
                 }
diff --git a/script/parser/compile.lua b/script/parser/compile.lua
index fc26bbcc3..41b0da17e 100644
--- a/script/parser/compile.lua
+++ b/script/parser/compile.lua
@@ -590,7 +590,7 @@ local function skipComment(isAction)
         local result, right = resolveLongString '*/'
         pushLongCommentError(left, right)
         State.comms[#State.comms+1] = {
-            type   = 'comment.long',
+            type   = 'comment.clong',
             start  = left,
             finish = right,
             text   = result,
diff --git a/script/parser/luadoc.lua b/script/parser/luadoc.lua
index 3fec5c182..8dc046f09 100644
--- a/script/parser/luadoc.lua
+++ b/script/parser/luadoc.lua
@@ -1713,10 +1713,9 @@ local function trimTailComment(text)
 end
 
 local function buildLuaDoc(comment)
-    local text = comment.text
-    local startPos = (comment.type == 'comment.short' and text:match '^%-%s*@()')
-                  or (comment.type == 'comment.long'  and text:match '^@()')
-    if not startPos then
+    local headPos = (comment.type == 'comment.short' and comment.text:match '^%-%s*@()')
+                 or (comment.type == 'comment.long'  and comment.text:match '^@()')
+    if not headPos then
         return {
             type    = 'doc.comment',
             start   = comment.start,
@@ -1725,14 +1724,15 @@ local function buildLuaDoc(comment)
             comment = comment,
         }
     end
-    local startOffset = comment.start
+    -- absolute position of `@` symbol
+    local startOffset = comment.start + headPos
     if comment.type == 'comment.long' then
-        startOffset = startOffset + #comment.mark - 2
+        startOffset = comment.start + headPos + #comment.mark - 2
     end
 
-    local doc = text:sub(startPos)
+    local doc = comment.text:sub(headPos)
 
-    parseTokens(doc, startOffset + startPos)
+    parseTokens(doc, startOffset)
     local result, rests = convertTokens(doc)
     if result then
         result.range = math.max(comment.finish, result.finish)
@@ -1743,14 +1743,14 @@ local function buildLuaDoc(comment)
                 finish = rest.firstFinish or result.finish
             end
         end
-        local cstart = text:find('%S', finish - comment.start)
-        if cstart and cstart < comment.finish then
+        local cstart = doc:find('%S', finish - startOffset)
+        if cstart then
             result.comment = {
                 type   = 'doc.tailcomment',
-                start  = cstart + comment.start,
+                start  = startOffset + cstart,
                 finish = comment.finish,
                 parent = result,
-                text   = trimTailComment(text:sub(cstart)),
+                text   = trimTailComment(doc:sub(cstart)),
             }
             if rests then
                 for _, rest in ipairs(rests) do
diff --git a/script/vm/sign.lua b/script/vm/sign.lua
index ddc8258f5..4f0fe1912 100644
--- a/script/vm/sign.lua
+++ b/script/vm/sign.lua
@@ -5,7 +5,7 @@ local vm            = require 'vm.vm'
 ---@class vm.sign
 ---@field parent    parser.object
 ---@field signList  vm.node[]
----@field docGenric parser.object[]
+---@field docGeneric parser.object[]
 local mt = {}
 mt.__index = mt
 mt.type = 'sign'
@@ -17,7 +17,7 @@ end
 
 ---@param doc parser.object
 function mt:addDocGeneric(doc)
-    self.docGenric[#self.docGenric+1] = doc
+    self.docGeneric[#self.docGeneric+1] = doc
 end
 
 ---@param uri uri
@@ -268,7 +268,7 @@ end
 function vm.createSign()
     local genericMgr = setmetatable({
         signList  = {},
-        docGenric = {},
+        docGeneric = {},
     }, mt)
     return genericMgr
 end

From cbe45e12aa5a22bad28bbf6867fe090b2fd63195 Mon Sep 17 00:00:00 2001
From: Shararvev <shararvev@gmail.com>
Date: Thu, 9 Jan 2025 06:34:04 +0500
Subject: [PATCH 18/34] #3028. Fix incorrect rest.range and finish on multiline
 types. Do not try to process a multiline `result`, while `doc` is the first
 line.

---
 script/parser/luadoc.lua | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/script/parser/luadoc.lua b/script/parser/luadoc.lua
index 8dc046f09..a96f9b061 100644
--- a/script/parser/luadoc.lua
+++ b/script/parser/luadoc.lua
@@ -1739,10 +1739,16 @@ local function buildLuaDoc(comment)
         local finish = result.firstFinish or result.finish
         if rests then
             for _, rest in ipairs(rests) do
-                rest.range = comment.finish
-                finish = rest.firstFinish or result.finish
+                rest.range = math.max(comment.finish, rest.finish)
+                finish = rest.firstFinish or rest.finish
             end
         end
+
+        -- `result` can be a multiline annotation or an alias, while `doc` is the first line, so we can't parse comment
+        if finish >= comment.finish then
+            return result, rests
+        end
+
         local cstart = doc:find('%S', finish - startOffset)
         if cstart then
             result.comment = {
@@ -1758,9 +1764,7 @@ local function buildLuaDoc(comment)
                 end
             end
         end
-    end
 
-    if result then
         return result, rests
     end
 

From 4af79e396c0ca7acf2dbcf8419366fc4e86465f8 Mon Sep 17 00:00:00 2001
From: Shararvev <shararvev@gmail.com>
Date: Thu, 9 Jan 2025 06:40:58 +0500
Subject: [PATCH 19/34] #3028. Support for a long strings with leading spaces
 #3034 (pattern is optional)

---
 script/core/semantic-tokens.lua | 2 +-
 script/parser/luadoc.lua        | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/script/core/semantic-tokens.lua b/script/core/semantic-tokens.lua
index ad4162e6f..3a34b758e 100644
--- a/script/core/semantic-tokens.lua
+++ b/script/core/semantic-tokens.lua
@@ -904,7 +904,7 @@ return function (uri, start, finish)
         if start <= comm.start and comm.finish <= finish then
             -- the same logic as in buildLuaDoc
             local headPos = (comm.type == 'comment.short' and comm.text:match '^%-%s*[@|]()')
-                         or (comm.type == 'comment.long'  and comm.text:match '^@()')
+                         or (comm.type == 'comment.long'  and comm.text:match '^%s*@()')
             if headPos then
                 -- absolute position of `@` symbol
                 local startOffset = comm.start + headPos
diff --git a/script/parser/luadoc.lua b/script/parser/luadoc.lua
index a96f9b061..ea47b63f0 100644
--- a/script/parser/luadoc.lua
+++ b/script/parser/luadoc.lua
@@ -1714,7 +1714,7 @@ end
 
 local function buildLuaDoc(comment)
     local headPos = (comment.type == 'comment.short' and comment.text:match '^%-%s*@()')
-                 or (comment.type == 'comment.long'  and comment.text:match '^@()')
+                 or (comment.type == 'comment.long'  and comment.text:match '^%s*@()')
     if not headPos then
         return {
             type    = 'doc.comment',

From 1b011f9f3973ae127114ac71e23646ccf5096917 Mon Sep 17 00:00:00 2001
From: Paul Emmerich <emmerich@google.com>
Date: Sun, 12 Jan 2025 10:40:51 +0100
Subject: [PATCH 20/34] Fix typo in doc exporter

---
 script/cli/doc/export.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/script/cli/doc/export.lua b/script/cli/doc/export.lua
index c56169f17..8d9b7d6f6 100644
--- a/script/cli/doc/export.lua
+++ b/script/cli/doc/export.lua
@@ -59,7 +59,7 @@ function export.getLocalPath(uri)
     local local_file_uri = uri
     local i, j = local_file_uri:find(DOC)
     if not j then
-        return '[FORIEGN]'..uri
+        return '[FOREIGN] '..uri
     end
     return local_file_uri:sub( j + 1 )
 end

From 914a55939a7dd2e9e88fda2f7dff9e7ebcca4761 Mon Sep 17 00:00:00 2001
From: Andrew Braxton <andrewcbraxton@gmail.com>
Date: Sun, 19 Jan 2025 02:43:21 -0500
Subject: [PATCH 21/34] Mark postfix completions as snippets

Similar to 3e0d1df. Postfix items should be marked as snippets.
---
 script/core/completion/postfix.lua | 1 +
 1 file changed, 1 insertion(+)

diff --git a/script/core/completion/postfix.lua b/script/core/completion/postfix.lua
index 46c24b8e7..5294f5436 100644
--- a/script/core/completion/postfix.lua
+++ b/script/core/completion/postfix.lua
@@ -363,6 +363,7 @@ local function checkPostFix(state, word, wordPosition, position, symbol, results
                         newText = newText,
                     },
                     sortText    = ('postfix-%04d'):format(i),
+                    insertTextFormat = 2,
 
                     additionalTextEdits = {
                         {

From 3ce90e014efa2262bbb4e34c6c686b04d5455cf6 Mon Sep 17 00:00:00 2001
From: Tom Lau <tomandfatboy@gmail.com>
Date: Thu, 16 Jan 2025 20:33:28 +0800
Subject: [PATCH 22/34] fix: bind variables with `@class` using tail comments

---
 changelog.md             | 1 +
 script/parser/luadoc.lua | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/changelog.md b/changelog.md
index 30926783a..313cb53bf 100644
--- a/changelog.md
+++ b/changelog.md
@@ -15,6 +15,7 @@
 * `FIX` Fixed the error that the configuration file pointed to by the `--configpath` option was not read and loaded.
 * `FIX` Generic return can be optional.
 * `FIX` Fixed the comment calculating in docs `---@param a string?Comment` - now its `Comment` instead of `omment`.
+* `FIX` Fixed cannot bind variables using tail comment `@class` [#2673](https://github.com/LuaLS/lua-language-server/issues/2673)
 * `NEW` `---@class` supports attribute `partial`, which will not check missing inherited fields [#3023](https://github.com/LuaLS/lua-language-server/issues/3023)
   ```lua
   ---@class Config
diff --git a/script/parser/luadoc.lua b/script/parser/luadoc.lua
index d0a1d38c6..72e5dc18d 100644
--- a/script/parser/luadoc.lua
+++ b/script/parser/luadoc.lua
@@ -2181,7 +2181,7 @@ local function bindDocs(state)
         if doc.specialBindGroup then
             bindDocWithSources(sources, doc.specialBindGroup)
             binded = nil
-        elseif isTailComment(text, doc) and doc.type ~= "doc.class" and doc.type ~= "doc.field" then
+        elseif isTailComment(text, doc) and doc.type ~= "doc.field" then
             bindDocWithSources(sources, binded)
             binded = nil
         else

From aa53e9a0d1abb045c4461ae03ed000b6be4f0318 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Eduard=20M=C3=BCller=20/=20taktik?=
 <11521600+emuell@users.noreply.github.com>
Date: Thu, 23 Jan 2025 17:06:55 +0100
Subject: [PATCH 23/34] Avoid truncating output in ---doc mode

fixes https://github.com/LuaLS/lua-language-server/issues/3049
---
 script/vm/infer.lua | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/script/vm/infer.lua b/script/vm/infer.lua
index 099b894b4..6f21a76ab 100644
--- a/script/vm/infer.lua
+++ b/script/vm/infer.lua
@@ -473,7 +473,8 @@ function mt:view(uri, default)
         end
     end
 
-    if #view > 200 then
+    -- do not truncate if exporting doc
+    if not DOC and #view > 200 then
         view = view:sub(1, 180) .. '...(too long)...' .. view:sub(-10)
     end
 

From d5135abc5edd19a83b2394c1e6c5a2cdbbcdd0da Mon Sep 17 00:00:00 2001
From: taktik <taktik@renoise.com>
Date: Thu, 23 Jan 2025 17:13:32 +0100
Subject: [PATCH 24/34] doc: update changelog

---
 changelog.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/changelog.md b/changelog.md
index 313cb53bf..10339536c 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 <!-- Add all new changes here. They will be moved under a version at release -->
+* `FIX` Don't truncate any output when running in `--doc` mode [#3049](https://github.com/LuaLS/lua-language-server/issues/3049)
 * `CHG` [#3014] Generic pattern now supports definition after capture and optional, union, array
   ```lua
   ---@generic T

From 2a7cc443067f6312af0fec9b0a1ac50bfdef2d69 Mon Sep 17 00:00:00 2001
From: Lewis Russell <lewis6991@gmail.com>
Date: Fri, 24 Jan 2025 09:37:51 +0000
Subject: [PATCH 25/34] refactor(cli/check): outline some functions and add
 some comments

---
 script/cli/check.lua        |  74 ++++++++++----------
 script/cli/check_worker.lua | 133 +++++++++++++++++++++---------------
 2 files changed, 116 insertions(+), 91 deletions(-)

diff --git a/script/cli/check.lua b/script/cli/check.lua
index c9a23b02c..3a5000428 100644
--- a/script/cli/check.lua
+++ b/script/cli/check.lua
@@ -7,6 +7,41 @@ local util       = require 'utility'
 
 local export = {}
 
+local function logFileForThread(threadId)
+    return LOGPATH .. '/check-partial-' .. threadId .. '.json'
+end
+
+local function buildArgs(exe, numThreads, threadId)
+    local args = {exe}
+    local skipNext = false
+    for i = 1, #arg do
+        local arg = arg[i]
+        -- --check needs to be transformed into --check_worker
+        if arg:lower():match('^%-%-check$') or arg:lower():match('^%-%-check=') then
+            args[#args + 1] = arg:gsub('%-%-%w*', '--check_worker')
+        -- --check_out_path needs to be removed if we have more than one thread
+        elseif arg:lower():match('%-%-check_out_path') and numThreads > 1 then
+            if not arg:match('%-%-%w*=') then
+                skipNext = true
+            end
+        else
+            if skipNext then
+                skipNext = false
+            else
+                args[#args + 1] = arg
+            end
+        end
+    end
+    args[#args + 1] = '--thread_id'
+    args[#args + 1] = tostring(threadId)
+    if numThreads > 1 then
+        args[#args + 1] = '--quiet'
+        args[#args + 1] = '--check_out_path'
+        args[#args + 1] = logFileForThread(threadId)
+    end
+    return args
+end
+
 function export.runCLI()
     local numThreads = tonumber(NUM_THREADS or 1)
 
@@ -21,48 +56,13 @@ function export.runCLI()
         exe = exe..'.exe'
     end
 
-    local function logFileForThread(threadId)
-        return LOGPATH .. '/check-partial-' .. threadId .. '.json'
-    end
-
-    local function buildArgs(threadId)
-        local args = {exe}
-        local skipNext = false
-        for i = 1, #arg do
-            local arg = arg[i]
-            -- --check needs to be transformed into --check_worker
-            if arg:lower():match('^%-%-check$') or arg:lower():match('^%-%-check=') then
-                args[#args + 1] = arg:gsub('%-%-%w*', '--check_worker')
-            -- --check_out_path needs to be removed if we have more than one thread
-            elseif arg:lower():match('%-%-check_out_path') and numThreads > 1 then
-                if not arg:match('%-%-%w*=') then
-                    skipNext = true
-                end
-            else
-                if skipNext then
-                    skipNext = false
-                else
-                    args[#args + 1] = arg
-                end
-            end
-        end
-        args[#args + 1] = '--thread_id'
-        args[#args + 1] = tostring(threadId)
-        if numThreads > 1 then
-            args[#args + 1] = '--quiet'
-            args[#args + 1] = '--check_out_path'
-            args[#args + 1] = logFileForThread(threadId)
-        end
-        return args
-    end
-
-    if numThreads > 1 then
+    if not QUIET and numThreads > 1 then
         print(lang.script('CLI_CHECK_MULTIPLE_WORKERS', numThreads))
     end
 
     local procs = {}
     for i = 1, numThreads do
-        local process, err = subprocess.spawn({buildArgs(i)})
+        local process, err = subprocess.spawn({buildArgs(exe, numThreads, i)})
         if err then
             print(err)
         end
diff --git a/script/cli/check_worker.lua b/script/cli/check_worker.lua
index a2e0bff11..a3139c3ba 100644
--- a/script/cli/check_worker.lua
+++ b/script/cli/check_worker.lua
@@ -17,6 +17,70 @@ require 'vm'
 
 local export = {}
 
+local function clear_line()
+    -- Write out empty space to ensure that the previous lien is cleared.
+    io.write('\x0D', (' '):rep(80), '\x0D')
+end
+
+--- @param i integer
+--- @param max integer
+--- @param results table<string, table[]>
+local function report_progress(i, max, results)
+    local filesWithErrors = 0
+    local errors = 0
+    for _, diags in pairs(results) do
+        filesWithErrors = filesWithErrors + 1
+        errors = errors + #diags
+    end
+
+    clear_line()
+    io.write(
+        ('>'):rep(math.ceil(i / max * 20)),
+        ('='):rep(20 - math.ceil(i / max * 20)),
+        ' ',
+        ('0'):rep(#tostring(max) - #tostring(i)),
+        tostring(i),
+        '/',
+        tostring(max)
+    )
+    if errors > 0 then
+        io.write(' [', lang.script('CLI_CHECK_PROGRESS', errors, filesWithErrors), ']')
+    end
+    io.flush()
+end
+
+--- @param uri string
+--- @param checkLevel integer
+local function apply_check_level(uri, checkLevel)
+    local config_disables = util.arrayToHash(config.get(uri, 'Lua.diagnostics.disable'))
+    local config_severities = config.get(uri, 'Lua.diagnostics.severity')
+    for name, serverity in pairs(define.DiagnosticDefaultSeverity) do
+        serverity = config_severities[name] or serverity
+        if serverity:sub(-1) == '!' then
+            serverity = serverity:sub(1, -2)
+        end
+        if define.DiagnosticSeverity[serverity] > checkLevel then
+            config_disables[name] = true
+        end
+    end
+    config.set(uri, 'Lua.diagnostics.disable', util.getTableKeys(config_disables, true))
+end
+
+local function downgrade_checks_to_opened(uri)
+    local diagStatus = config.get(uri, 'Lua.diagnostics.neededFileStatus')
+    for d, status in pairs(diagStatus) do
+        if status == 'Any' or status == 'Any!' then
+            diagStatus[d] = 'Opened!'
+        end
+    end
+    for d, status in pairs(protoDiag.getDefaultStatus()) do
+        if status == 'Any' or status == 'Any!' then
+            diagStatus[d] = 'Opened!'
+        end
+    end
+    config.set(uri, 'Lua.diagnostics.neededFileStatus', diagStatus)
+end
+
 function export.runCLI()
     lang(LOCALE)
 
@@ -36,18 +100,16 @@ function export.runCLI()
     end
     rootUri = rootUri:gsub("/$", "")
 
-    if CHECKLEVEL then
-        if not define.DiagnosticSeverity[CHECKLEVEL] then
-            print(lang.script('CLI_CHECK_ERROR_LEVEL', 'Error, Warning, Information, Hint'))
-            return
-        end
+    if CHECKLEVEL and not define.DiagnosticSeverity[CHECKLEVEL] then
+        print(lang.script('CLI_CHECK_ERROR_LEVEL', 'Error, Warning, Information, Hint'))
+        return
     end
     local checkLevel = define.DiagnosticSeverity[CHECKLEVEL] or define.DiagnosticSeverity.Warning
 
     util.enableCloseFunction()
 
     local lastClock = os.clock()
-    local results = {}
+    local results = {}  --- @type table<string, table[]>
 
     local function errorhandler(err)
         print(err)
@@ -75,31 +137,12 @@ function export.runCLI()
 
         ws.awaitReady(rootUri)
 
-        local disables = util.arrayToHash(config.get(rootUri, 'Lua.diagnostics.disable'))
-        for name, serverity in pairs(define.DiagnosticDefaultSeverity) do
-            serverity = config.get(rootUri, 'Lua.diagnostics.severity')[name] or serverity
-            if serverity:sub(-1) == '!' then
-                serverity = serverity:sub(1, -2)
-            end
-            if define.DiagnosticSeverity[serverity] > checkLevel then
-                disables[name] = true
-            end
-        end
-        config.set(rootUri, 'Lua.diagnostics.disable', util.getTableKeys(disables, true))
+        -- Disable any diagnostics that are above the check level
+        apply_check_level(rootUri, checkLevel)
 
-        -- Downgrade file opened status to Opened for everything to avoid reporting during compilation on files that do not belong to this thread
-        local diagStatus = config.get(rootUri, 'Lua.diagnostics.neededFileStatus')
-        for diag, status in pairs(diagStatus) do
-            if status == 'Any' or status == 'Any!' then
-                diagStatus[diag] = 'Opened!'
-            end
-        end
-        for diag, status in pairs(protoDiag.getDefaultStatus()) do
-            if status == 'Any' or status == 'Any!' then
-                diagStatus[diag] = 'Opened!'
-            end
-        end
-        config.set(rootUri, 'Lua.diagnostics.neededFileStatus', diagStatus)
+        -- Downgrade file opened status to Opened for everything to avoid
+        -- reporting during compilation on files that do not belong to this thread
+        downgrade_checks_to_opened(rootUri)
 
         local uris = files.getChildFiles(rootUri)
         local max  = #uris
@@ -108,28 +151,12 @@ function export.runCLI()
             if (i % numThreads + 1) == threadId then
                 files.open(uri)
                 diag.doDiagnostic(uri, true)
-                -- Print regularly but always print the last entry to ensure that logs written to files don't look incomplete.
-                if (os.clock() - lastClock > 0.2 or i == #uris) and not QUIET then
+                -- Print regularly but always print the last entry to ensure
+                -- that logs written to files don't look incomplete.
+                if not QUIET and (os.clock() - lastClock > 0.2 or i == #uris) then
                     lastClock = os.clock()
                     client:update()
-                    local output = '\x0D'
-                                .. ('>'):rep(math.ceil(i / max * 20))
-                                .. ('='):rep(20 - math.ceil(i / max * 20))
-                                .. ' '
-                                .. ('0'):rep(#tostring(max) - #tostring(i))
-                                .. tostring(i) .. '/' .. tostring(max)
-                    io.write(output)
-                    local filesWithErrors = 0
-                    local errors = 0
-                    for _, diags in pairs(results) do
-                        filesWithErrors = filesWithErrors + 1
-                        errors = errors + #diags
-                    end
-                    if errors > 0 then
-                        local errorDetails = ' [' .. lang.script('CLI_CHECK_PROGRESS', errors, filesWithErrors) .. ']'
-                        io.write(errorDetails)
-                    end
-                    io.flush()
+                    report_progress(i, max, results)
                 end
             end
         end
@@ -146,10 +173,8 @@ function export.runCLI()
         end
     end
 
-    local outpath = CHECK_OUT_PATH
-    if outpath == nil then
-        outpath = LOGPATH .. '/check.json'
-    end
+    local outpath = CHECK_OUT_PATH or LOGPATH .. '/check.json'
+
     -- Always write result, even if it's empty to make sure no one accidentally looks at an old output after a successful run.
     util.saveFile(outpath, jsonb.beautify(results))
 

From a2ddc19e1e3f0b8eb20255cf5f981893ab3da5fa Mon Sep 17 00:00:00 2001
From: Tom Lau <tomandfatboy@gmail.com>
Date: Tue, 21 Jan 2025 14:02:57 +0800
Subject: [PATCH 26/34] feat: add folder name concept to `workspace.scope`

---
 script/provider/provider.lua   |  2 +-
 script/workspace/scope.lua     | 10 +++++++++-
 script/workspace/workspace.lua |  4 ++--
 3 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/script/provider/provider.lua b/script/provider/provider.lua
index 8b988059e..38aa73031 100644
--- a/script/provider/provider.lua
+++ b/script/provider/provider.lua
@@ -119,7 +119,7 @@ m.register 'initialize' {
 
         if params.workspaceFolders then
             for _, folder in ipairs(params.workspaceFolders) do
-                workspace.create(files.getRealUri(folder.uri))
+                workspace.create(files.getRealUri(folder.uri), folder.name)
             end
         elseif params.rootUri then
             workspace.create(files.getRealUri(params.rootUri))
diff --git a/script/workspace/scope.lua b/script/workspace/scope.lua
index cfdfdc909..360f6e28d 100644
--- a/script/workspace/scope.lua
+++ b/script/workspace/scope.lua
@@ -8,6 +8,7 @@ local m = {}
 ---@class scope
 ---@field type   scope.type
 ---@field uri?   uri
+---@field folderName? string
 ---@field _links table<uri, boolean>
 ---@field _data  table<string, any>
 ---@field _gc    gc
@@ -134,6 +135,11 @@ function mt:getName()
     return self.uri or ('<' .. self.type .. '>')
 end
 
+---@return string?
+function mt:getFolderName()
+    return self.folderName
+end
+
 function mt:gc(obj)
     self._gc:add(obj)
 end
@@ -187,10 +193,12 @@ end
 m.reset()
 
 ---@param uri uri
+---@param folderName? string
 ---@return scope
-function m.createFolder(uri)
+function m.createFolder(uri, folderName)
     local scope = createScope 'folder'
     scope.uri = uri
+    scope.folderName = folderName
 
     local inserted = false
     for i, otherScope in ipairs(m.folders) do
diff --git a/script/workspace/workspace.lua b/script/workspace/workspace.lua
index fe7c6c151..4ae3ba9e8 100644
--- a/script/workspace/workspace.lua
+++ b/script/workspace/workspace.lua
@@ -44,9 +44,9 @@ function m.initRoot(uri)
 end
 
 --- 初始化工作区
-function m.create(uri)
+function m.create(uri, folderName)
     log.info('Workspace create: ', uri)
-    local scp = scope.createFolder(uri)
+    local scp = scope.createFolder(uri, folderName)
     m.folders[#m.folders+1] = scp
     if uri == furi.encode '/'
     or uri == furi.encode(os.getenv 'HOME' or '') then

From 41ffcf2e97a8c9627eefa48b86f872079c84aa45 Mon Sep 17 00:00:00 2001
From: Tom Lau <tomandfatboy@gmail.com>
Date: Sat, 25 Jan 2025 09:57:17 +0800
Subject: [PATCH 27/34] feat: add variable substitution support for
 `${workspaceFolder:x}`

---
 script/files.lua | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/script/files.lua b/script/files.lua
index 9284708b3..f61330e21 100644
--- a/script/files.lua
+++ b/script/files.lua
@@ -945,6 +945,17 @@ function m.resolvePathPlaceholders(path)
         elseif key:sub(1, 4) == "env:" then
             local env = os.getenv(key:sub(5))
             return env
+        elseif key == "workspaceFolder" then
+            local ws = require 'workspace'
+            return ws.rootUri and furi.decode(ws.rootUri)
+        elseif key:match("^workspaceFolder:.+$") then
+            local folderName = key:match("^workspaceFolder:(.+)$")
+            for _, scp in ipairs(scope.folders) do
+                if scp:getFolderName() == folderName then
+                    return scp.uri and furi.decode(scp.uri)
+                end
+            end
+            log.warn(('variable ${%s} cannot be resolved when processing path: %s'):format(key, path))
         end
     end)
 

From 892795521bc66c6a1143df82dedcf9d9c914a45e Mon Sep 17 00:00:00 2001
From: Tom Lau <tomandfatboy@gmail.com>
Date: Sat, 25 Jan 2025 10:05:37 +0800
Subject: [PATCH 28/34] doc: update changelog

---
 changelog.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/changelog.md b/changelog.md
index 10339536c..0e4bd1795 100644
--- a/changelog.md
+++ b/changelog.md
@@ -31,6 +31,7 @@
   cfgs[2] = {}        -- only warns missing `b`
   ```
   This enables the previous missing field check behavior before [#2970](https://github.com/LuaLS/lua-language-server/issues/2970)
+* `NEW` Added variable substitution support for vscode's `${workspaceFolder:x}` when resolving path placeholders [#2987](https://github.com/LuaLS/lua-language-server/issues/2987)
 
 ## 3.13.5
 `2024-12-20`

From 1545d8d314e7d9f942787571534a55b41b8bca73 Mon Sep 17 00:00:00 2001
From: Lewis Russell <lewis6991@gmail.com>
Date: Thu, 23 Jan 2025 14:29:04 +0000
Subject: [PATCH 29/34] feat: add --check_format=json|pretty

Adds a new CLI option --check_format which accepts the values: 'json'
(current behaviour), and a new 'pretty' value which prints a colorized
human readable report to stdout, similar to common compilers and
linters.

Results are printed with color unless NO_COLOR is defined in the users
environment, as per https://no-color.org.

If --check_out_path is provided, then the results are always saved to
file regardless of the --check_format value.
---
 changelog.md                |   1 +
 locale/en-us/script.lua     |   4 +-
 locale/ja-jp/script.lua     |   4 +-
 locale/pt-br/script.lua     |   4 +-
 locale/zh-cn/script.lua     |   4 +-
 locale/zh-tw/script.lua     |   4 +-
 script/cli/check.lua        |  37 +++++++----
 script/cli/check_worker.lua | 122 +++++++++++++++++++++++++++++++++---
 script/global.d.lua         |   3 +
 9 files changed, 156 insertions(+), 27 deletions(-)

diff --git a/changelog.md b/changelog.md
index 313cb53bf..89a2bf6fe 100644
--- a/changelog.md
+++ b/changelog.md
@@ -30,6 +30,7 @@
   cfgs[2] = {}        -- only warns missing `b`
   ```
   This enables the previous missing field check behavior before [#2970](https://github.com/LuaLS/lua-language-server/issues/2970)
+* `NEW` Added `--check_format=json|pretty` for use with `--check` to output diagnostics in a human readable format.
 
 ## 3.13.5
 `2024-12-20`
diff --git a/locale/en-us/script.lua b/locale/en-us/script.lua
index 9c9163ae4..ee96900c7 100644
--- a/locale/en-us/script.lua
+++ b/locale/en-us/script.lua
@@ -648,8 +648,10 @@ CLI_CHECK_SUCCESS =
 'Diagnosis completed, no problems found'
 CLI_CHECK_PROGRESS =
 'Found {} problems in {} files'
-CLI_CHECK_RESULTS =
+CLI_CHECK_RESULTS_OUTPATH =
 'Diagnosis complete, {} problems found, see {}'
+CLI_CHECK_RESULTS_PRETTY =
+'Diagnosis complete, {} problems found'
 CLI_CHECK_MULTIPLE_WORKERS =
 'Starting {} worker tasks, progress output will be disabled. This may take a few minutes.'
 CLI_DOC_INITING   =
diff --git a/locale/ja-jp/script.lua b/locale/ja-jp/script.lua
index 3c0cd48d6..6e1819d98 100644
--- a/locale/ja-jp/script.lua
+++ b/locale/ja-jp/script.lua
@@ -649,8 +649,10 @@ CLI_CHECK_SUCCESS =
 '診断が完了しました。問題は見つかりませんでした'
 CLI_CHECK_PROGRESS =
 '{} ファイルに渡り、{} 個の問題が発見されました'
-CLI_CHECK_RESULTS =
+CLI_CHECK_RESULTS_OUTPATH =
 '診断が完了しました。{} 個の問題が発見されました。詳しくは {} をご確認ください'
+CLI_CHECK_RESULTS_PRETTY =
+'診断が完了しました。{} 個の問題が発見されました'
 CLI_CHECK_MULTIPLE_WORKERS =
 '{} 個のワーカータスクを開始しているため、進行状況の出力が無効になります。完了まで数分かかることがあります。'
 CLI_DOC_INITING   =
diff --git a/locale/pt-br/script.lua b/locale/pt-br/script.lua
index e763fb6c4..73d459d75 100644
--- a/locale/pt-br/script.lua
+++ b/locale/pt-br/script.lua
@@ -648,8 +648,10 @@ CLI_CHECK_SUCCESS =
 'Diagnóstico completo, nenhum problema encontrado'
 CLI_CHECK_PROGRESS = -- TODO: need translate!
 'Found {} problems in {} files'
-CLI_CHECK_RESULTS =
+CLI_CHECK_RESULTS_OUTPATH =
 'Diagnóstico completo, {} problemas encontrados, veja {}'
+CLI_CHECK_RESULTS_PRETTY =
+'Diagnóstico completo, {} problemas encontrados'
 CLI_CHECK_MULTIPLE_WORKERS = -- TODO: need translate!
 'Starting {} worker tasks, progress output will be disabled. This may take a few minutes.'
 CLI_DOC_INITING   = -- TODO: need translate!
diff --git a/locale/zh-cn/script.lua b/locale/zh-cn/script.lua
index 0b748d84f..22f16eaac 100644
--- a/locale/zh-cn/script.lua
+++ b/locale/zh-cn/script.lua
@@ -648,8 +648,10 @@ CLI_CHECK_SUCCESS =
 '诊断完成，没有发现问题'
 CLI_CHECK_PROGRESS =
 '检测到问题 {} 在文件 {} 中'
-CLI_CHECK_RESULTS =
+CLI_CHECK_RESULTS_OUTPATH =
 '诊断完成，共有 {} 个问题，请查看 {}'
+CLI_CHECK_RESULTS_PRETTY =
+'诊断完成，共有 {} 个问题'
 CLI_CHECK_MULTIPLE_WORKERS =
 '开启 {} 个工作任务，进度输出将会被禁用。这可能会花费几分钟。'
 CLI_DOC_INITING   =
diff --git a/locale/zh-tw/script.lua b/locale/zh-tw/script.lua
index 31e885208..090fb7ff0 100644
--- a/locale/zh-tw/script.lua
+++ b/locale/zh-tw/script.lua
@@ -648,8 +648,10 @@ CLI_CHECK_SUCCESS =
 '診斷完成，沒有發現問題'
 CLI_CHECK_PROGRESS = -- TODO: need translate!
 'Found {} problems in {} files'
-CLI_CHECK_RESULTS =
+CLI_CHECK_RESULTS_OUTPATH =
 '診斷完成，共有 {} 個問題，請查看 {}'
+CLI_CHECK_RESULTS_PRETTY =
+'診斷完成，共有 {} 個問題'
 CLI_CHECK_MULTIPLE_WORKERS = -- TODO: need translate!
 'Starting {} worker tasks, progress output will be disabled. This may take a few minutes.'
 CLI_DOC_INITING   = -- TODO: need translate!
diff --git a/script/cli/check.lua b/script/cli/check.lua
index 3a5000428..7f38dfea4 100644
--- a/script/cli/check.lua
+++ b/script/cli/check.lua
@@ -11,7 +11,7 @@ local function logFileForThread(threadId)
     return LOGPATH .. '/check-partial-' .. threadId .. '.json'
 end
 
-local function buildArgs(exe, numThreads, threadId)
+local function buildArgs(exe, numThreads, threadId, format, quiet)
     local args = {exe}
     local skipNext = false
     for i = 1, #arg do
@@ -35,7 +35,12 @@ local function buildArgs(exe, numThreads, threadId)
     args[#args + 1] = '--thread_id'
     args[#args + 1] = tostring(threadId)
     if numThreads > 1 then
-        args[#args + 1] = '--quiet'
+        if quiet then
+            args[#args + 1] = '--quiet'
+        end
+        if format then
+            args[#args + 1] = '--check_format=' .. format
+        end
         args[#args + 1] = '--check_out_path'
         args[#args + 1] = logFileForThread(threadId)
     end
@@ -62,7 +67,7 @@ function export.runCLI()
 
     local procs = {}
     for i = 1, numThreads do
-        local process, err = subprocess.spawn({buildArgs(exe, numThreads, i)})
+        local process, err = subprocess.spawn({buildArgs(exe, numThreads, i, CHECK_FORMAT, QUIET)})
         if err then
             print(err)
         end
@@ -76,11 +81,6 @@ function export.runCLI()
         checkPassed = process:wait() == 0 and checkPassed
     end
 
-    local outpath = CHECK_OUT_PATH
-    if outpath == nil then
-        outpath = LOGPATH .. '/check.json'
-    end
-
     if numThreads > 1 then
         local mergedResults = {}
         local count = 0
@@ -95,11 +95,22 @@ function export.runCLI()
                 end
             end
         end
-        util.saveFile(outpath, jsonb.beautify(mergedResults))
-        if count == 0 then
-            print(lang.script('CLI_CHECK_SUCCESS'))
-        else
-            print(lang.script('CLI_CHECK_RESULTS', count, outpath))
+
+        local outpath = nil
+
+        if CHECK_FORMAT == 'json' or CHECK_OUT_PATH then
+            outpath = CHECK_OUT_PATH or LOGPATH .. '/check.json'
+            util.saveFile(outpath, jsonb.beautify(mergedResults))
+        end
+
+        if not QUIET then
+            if count == 0 then
+                print(lang.script('CLI_CHECK_SUCCESS'))
+            elseif outpath then
+                print(lang.script('CLI_CHECK_RESULTS_OUTPATH', count, outpath))
+            else
+                print(lang.script('CLI_CHECK_RESULTS_PRETTY', count))
+            end
         end
     end
     return checkPassed and 0 or 1
diff --git a/script/cli/check_worker.lua b/script/cli/check_worker.lua
index a3139c3ba..353926395 100644
--- a/script/cli/check_worker.lua
+++ b/script/cli/check_worker.lua
@@ -17,6 +17,100 @@ require 'vm'
 
 local export = {}
 
+local colors
+
+if not os.getenv('NO_COLOR') then
+    colors = {
+        red = '\27[31m',
+        green = '\27[32m',
+        yellow = '\27[33m',
+        blue = '\27[34m',
+        magenta = '\27[35m',
+        white = '\27[37m',
+        grey = '\27[90m',
+        reset = '\27[0m'
+    }
+else
+    colors = {
+        red = '',
+        green = '',
+        yellow = '',
+        blue = '',
+        magenta = '',
+        white = '',
+        grey = '',
+        reset = ''
+    }
+end
+
+--- @type table<DiagnosticSeverity, string>
+local severity_colors = {
+    Error = colors.red,
+    Warning = colors.yellow,
+    Information = colors.white,
+    Hint = colors.white,
+}
+
+local severity_str = {} --- @type table<integer,DiagnosticSeverity>
+for k, v in pairs(define.DiagnosticSeverity) do
+    severity_str[v] = k
+end
+
+local pwd
+
+---@param path string
+---@return string
+local function relpath(path)
+    if not pwd then
+        pwd = furi.decode(furi.encode(fs.current_path():string()))
+    end
+    if pwd and path:sub(1, #pwd) == pwd then
+        path = path:sub(#pwd + 2)
+    end
+    return path
+end
+
+local function report_pretty(uri, diags)
+    local path = relpath(furi.decode(uri))
+
+    local lines = {} --- @type string[]
+    pcall(function()
+        for line in io.lines(path) do
+            table.insert(lines, line)
+        end
+    end)
+
+    for _, d in ipairs(diags) do
+        local rstart = d.range.start
+        local rend = d.range['end']
+        local severity = severity_str[d.severity]
+        print(
+            ('%s%s:%s:%s%s [%s%s%s] %s %s(%s)%s'):format(
+                colors.blue,
+                path,
+                rstart.line + 1, -- Use 1-based indexing
+                rstart.character + 1, -- Use 1-based indexing
+                colors.reset,
+                severity_colors[severity],
+                severity,
+                colors.reset,
+                d.message,
+                colors.magenta,
+                d.code,
+                colors.reset
+            )
+        )
+        if #lines > 0 then
+            io.write('    ', lines[rstart.line + 1], '\n')
+            io.write('    ', colors.grey, (' '):rep(rstart.character), '^')
+            if rstart.line == rend.line then
+                io.write(('^'):rep(rend.character - rstart.character - 1))
+            end
+            io.write(colors.reset, '\n')
+        end
+    end
+end
+
 local function clear_line()
     -- Write out empty space to ensure that the previous lien is cleared.
     io.write('\x0D', (' '):rep(80), '\x0D')
@@ -86,6 +180,7 @@ function export.runCLI()
 
     local numThreads = tonumber(NUM_THREADS or 1)
     local threadId = tonumber(THREAD_ID or 1)
+    local quiet = QUIET or numThreads > 1
 
     if type(CHECK_WORKER) ~= 'string' then
         print(lang.script('CLI_CHECK_ERROR_TYPE', type(CHECK_WORKER)))
@@ -127,9 +222,13 @@ function export.runCLI()
 
         client:register('textDocument/publishDiagnostics', function (params)
             results[params.uri] = params.diagnostics
+            if not QUIET and (CHECK_FORMAT == nil or CHECK_FORMAT == 'pretty') then
+                clear_line()
+                report_pretty(params.uri, params.diagnostics)
+            end
         end)
 
-        if not QUIET then
+        if not quiet then
             io.write(lang.script('CLI_CHECK_INITING'))
         end
 
@@ -153,15 +252,15 @@ function export.runCLI()
                 diag.doDiagnostic(uri, true)
                 -- Print regularly but always print the last entry to ensure
                 -- that logs written to files don't look incomplete.
-                if not QUIET and (os.clock() - lastClock > 0.2 or i == #uris) then
+                if not quiet and (os.clock() - lastClock > 0.2 or i == #uris) then
                     lastClock = os.clock()
                     client:update()
                     report_progress(i, max, results)
                 end
             end
         end
-        if not QUIET then
-            io.write('\x0D')
+        if not quiet then
+            clear_line()
         end
     end)
 
@@ -173,16 +272,21 @@ function export.runCLI()
         end
     end
 
-    local outpath = CHECK_OUT_PATH or LOGPATH .. '/check.json'
+    local outpath = nil
 
-    -- Always write result, even if it's empty to make sure no one accidentally looks at an old output after a successful run.
-    util.saveFile(outpath, jsonb.beautify(results))
+    if CHECK_FORMAT == 'json' or CHECK_OUT_PATH then
+        outpath = CHECK_OUT_PATH or LOGPATH .. '/check.json'
+        -- Always write result, even if it's empty to make sure no one accidentally looks at an old output after a successful run.
+        util.saveFile(outpath, jsonb.beautify(results))
+    end
 
-    if not QUIET then
+    if not quiet then
         if count == 0 then
             print(lang.script('CLI_CHECK_SUCCESS'))
+        elseif outpath then
+            print(lang.script('CLI_CHECK_RESULTS_OUTPATH', count, outpath))
         else
-            print(lang.script('CLI_CHECK_RESULTS', count, outpath))
+            print(lang.script('CLI_CHECK_RESULTS_PRETTY', count))
         end
     end
     return count == 0 and 0 or 1
diff --git a/script/global.d.lua b/script/global.d.lua
index daac5f6c8..f4e4b336b 100644
--- a/script/global.d.lua
+++ b/script/global.d.lua
@@ -69,6 +69,9 @@ CHECKLEVEL = 'Warning'
 ---@type string|nil
 CHECK_OUT_PATH = ''
 
+---@type string | 'json' | 'pretty'
+CHECK_FORMAT = 'pretty'
+
 ---@type 'trace' | 'debug' | 'info' | 'warn' | 'error'
 LOGLEVEL = 'warn'
 

From 43adedad590f051e92ba88d0ec5779ac1b72817f Mon Sep 17 00:00:00 2001
From: Tom Lau <tomandfatboy@gmail.com>
Date: Sat, 25 Jan 2025 11:52:32 +0800
Subject: [PATCH 30/34] fix: missing field completion for generic class object

---
 script/vm/compiler.lua | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua
index 92da538e5..cee5de55e 100644
--- a/script/vm/compiler.lua
+++ b/script/vm/compiler.lua
@@ -304,6 +304,17 @@ local searchFieldSwitch = util.switch()
             end
         end
     end)
+    : case 'doc.type.sign'
+    : call(function (suri, source, key, pushResult)
+        if not source.node[1] then
+            return
+        end
+        local global = vm.getGlobal('type', source.node[1])
+        if not global then
+            return
+        end
+        vm.getClassFields(suri, global, key, pushResult)
+    end)
     : case 'global'
     : call(function (suri, node, key, pushResult)
         if node.cate == 'variable' then

From c3d5164b41c2dce75ef1fce4cd535d4c5d0ffb9c Mon Sep 17 00:00:00 2001
From: Tom Lau <tomandfatboy@gmail.com>
Date: Sat, 25 Jan 2025 18:00:25 +0800
Subject: [PATCH 31/34] test: add tests

---
 test/completion/common.lua | 41 ++++++++++++++++++++++++++++++++++++++
 test/completion/init.lua   | 14 +++++++++++--
 2 files changed, 53 insertions(+), 2 deletions(-)

diff --git a/test/completion/common.lua b/test/completion/common.lua
index 7f576e0bf..32cc68481 100644
--- a/test/completion/common.lua
+++ b/test/completion/common.lua
@@ -4572,3 +4572,44 @@ M.create():optional(<??>):self()
         kind  = define.CompletionItemKind.EnumMember,
     },
 }
+
+TEST [[
+---@class Array<T>: { [integer]: T }
+---@field length integer
+local Array
+
+function Array:push() end
+
+---@type Array<string>
+local a
+print(a.<??>)
+]]
+{
+    include = true,
+    {
+        label = 'length',
+        kind  = define.CompletionItemKind.Field,
+    },
+    {
+        label = 'push(self)',
+        kind  = define.CompletionItemKind.Method,
+    },
+}
+
+TEST [[
+---@class Array<T>: { [integer]: T }
+---@field length integer
+local Array
+
+function Array:push() end
+
+---@type Array<string>
+local a
+print(a:<??>)
+]]
+{
+    {
+        label = 'push()',
+        kind  = define.CompletionItemKind.Method,
+    },
+}
diff --git a/test/completion/init.lua b/test/completion/init.lua
index 1ac7fd4b3..81fd080b8 100644
--- a/test/completion/init.lua
+++ b/test/completion/init.lua
@@ -40,8 +40,18 @@ local function include(a, b)
         return false
     end
     if tp1 == 'table' then
-        for k in pairs(a) do
-            if not eq(a[k], b[k]) then
+        -- a / b are array of completion results
+        -- when checking `include`, the array index order is not important
+        -- thus need to check every results in b
+        for _, v1 in ipairs(a) do
+            local ok = false
+            for _, v2 in ipairs(b) do
+                if eq(v1, v2) then
+                    ok = true
+                    break
+                end
+            end
+            if not ok then
                 return false
             end
         end

From 3a8203e9ee06fa327ff96e0ed000dd9dbb4f3780 Mon Sep 17 00:00:00 2001
From: Tom Lau <tomandfatboy@gmail.com>
Date: Sat, 25 Jan 2025 18:12:27 +0800
Subject: [PATCH 32/34] doc: update changelog

---
 changelog.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/changelog.md b/changelog.md
index f19b377b5..1b6f39476 100644
--- a/changelog.md
+++ b/changelog.md
@@ -17,6 +17,7 @@
 * `FIX` Generic return can be optional.
 * `FIX` Fixed the comment calculating in docs `---@param a string?Comment` - now its `Comment` instead of `omment`.
 * `FIX` Fixed cannot bind variables using tail comment `@class` [#2673](https://github.com/LuaLS/lua-language-server/issues/2673)
+* `FIX` Fixed missing field completion for generic class object [#2196](https://github.com/LuaLS/lua-language-server/issues/2196) [#2945](https://github.com/LuaLS/lua-language-server/issues/2945) [#3041](https://github.com/LuaLS/lua-language-server/issues/3041)
 * `NEW` `---@class` supports attribute `partial`, which will not check missing inherited fields [#3023](https://github.com/LuaLS/lua-language-server/issues/3023)
   ```lua
   ---@class Config

From 54b132317c607b8cdb9f58901567bd4096324e16 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=80=E8=90=8C=E5=B0=8F=E6=B1=90?= <sumneko@hotmail.com>
Date: Thu, 6 Feb 2025 15:12:32 +0800
Subject: [PATCH 33/34] update locale of `command.startServer`

---
 locale/en-us/setting.lua | 4 ++--
 locale/ja-jp/setting.lua | 4 ++--
 locale/pt-br/setting.lua | 4 ++--
 locale/zh-cn/setting.lua | 4 ++--
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/locale/en-us/setting.lua b/locale/en-us/setting.lua
index 3dfdaceb8..095673f86 100644
--- a/locale/en-us/setting.lua
+++ b/locale/en-us/setting.lua
@@ -458,6 +458,6 @@ command.addon_manager.open =
 command.reloadFFIMeta =
 'Lua: Reload luajit ffi meta'
 command.startServer =
-'Lua: (debug) Start Language Server'
+'Lua: Restart Language Server'
 command.stopServer =
-'Lua: (debug) Stop Language Server'
+'Lua: Stop Language Server'
diff --git a/locale/ja-jp/setting.lua b/locale/ja-jp/setting.lua
index afa05b641..a522ff944 100644
--- a/locale/ja-jp/setting.lua
+++ b/locale/ja-jp/setting.lua
@@ -458,6 +458,6 @@ command.addon_manager.open = -- TODO: need translate!
 command.reloadFFIMeta = -- TODO: need translate!
 'Lua: Reload luajit ffi meta'
 command.startServer = -- TODO: need translate!
-'Lua: (debug) Start Language Server'
+'Lua: Restart Language Server'
 command.stopServer = -- TODO: need translate!
-'Lua: (debug) Stop Language Server'
+'Lua: Stop Language Server'
diff --git a/locale/pt-br/setting.lua b/locale/pt-br/setting.lua
index cce2e96c5..ca04fdc08 100644
--- a/locale/pt-br/setting.lua
+++ b/locale/pt-br/setting.lua
@@ -458,6 +458,6 @@ command.addon_manager.open = -- TODO: need translate!
 command.reloadFFIMeta = -- TODO: need translate!
 'Lua: Reload luajit ffi meta'
 command.startServer = -- TODO: need translate!
-'Lua: (debug) Start Language Server'
+'Lua: Restart Language Server'
 command.stopServer = -- TODO: need translate!
-'Lua: (debug) Stop Language Server'
+'Lua: Stop Language Server'
diff --git a/locale/zh-cn/setting.lua b/locale/zh-cn/setting.lua
index a55f8d42e..b610ac196 100644
--- a/locale/zh-cn/setting.lua
+++ b/locale/zh-cn/setting.lua
@@ -456,6 +456,6 @@ command.addon_manager.open =
 command.reloadFFIMeta =
 'Lua: 重新生成luajit的FFI模块C语言元数据'
 command.startServer =
-'Lua: (debug) 启动服务器'
+'Lua: 重启语言服务器'
 command.stopServer =
-'Lua: (debug) 停止服务器'
+'Lua: 停止语言服务器'

From b0b5c8f17fdcb6f8d6244eca48b8fc053e9b15ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=80=E8=90=8C=E5=B0=8F=E6=B1=90?= <sumneko@hotmail.com>
Date: Thu, 6 Feb 2025 15:14:12 +0800
Subject: [PATCH 34/34] 3.13.6

---
 changelog.md | 33 ++++++++++++++++++---------------
 1 file changed, 18 insertions(+), 15 deletions(-)

diff --git a/changelog.md b/changelog.md
index ba7cd1fd1..baa1210fc 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,22 +2,9 @@
 
 ## Unreleased
 <!-- Add all new changes here. They will be moved under a version at release -->
-* `FIX` Don't truncate any output when running in `--doc` mode [#3049](https://github.com/LuaLS/lua-language-server/issues/3049)
-* `CHG` [#3014] Generic pattern now supports definition after capture and optional, union, array
-  ```lua
-  ---@generic T
-  ---@param t `T`.Cat?
-  ---@return T?
-  local function f(t) end
 
-  local t = f('Smile') --> t is `(Smile.Cat)?`
-  ```
-* `NEW` Test CLI: `--name=<testname>` `-n=<testname>`: run specify unit test
-* `FIX` Fixed the error that the configuration file pointed to by the `--configpath` option was not read and loaded.
-* `FIX` Generic return can be optional.
-* `FIX` Fixed the comment calculating in docs `---@param a string?Comment` - now its `Comment` instead of `omment`.
-* `FIX` Fixed cannot bind variables using tail comment `@class` [#2673](https://github.com/LuaLS/lua-language-server/issues/2673)
-* `FIX` Fixed missing field completion for generic class object [#2196](https://github.com/LuaLS/lua-language-server/issues/2196) [#2945](https://github.com/LuaLS/lua-language-server/issues/2945) [#3041](https://github.com/LuaLS/lua-language-server/issues/3041)
+## 3.13.6
+`2025-2-6`
 * `NEW` `---@class` supports attribute `partial`, which will not check missing inherited fields [#3023](https://github.com/LuaLS/lua-language-server/issues/3023)
   ```lua
   ---@class Config
@@ -34,6 +21,22 @@
   This enables the previous missing field check behavior before [#2970](https://github.com/LuaLS/lua-language-server/issues/2970)
 * `NEW` Added variable substitution support for vscode's `${workspaceFolder:x}` when resolving path placeholders [#2987](https://github.com/LuaLS/lua-language-server/issues/2987)
 * `NEW` Added `--check_format=json|pretty` for use with `--check` to output diagnostics in a human readable format.
+* `NEW` Test CLI: `--name=<testname>` `-n=<testname>`: run specify unit test
+* `CHG` [#3014] Generic pattern now supports definition after capture and optional, union, array
+  ```lua
+  ---@generic T
+  ---@param t `T`.Cat?
+  ---@return T?
+  local function f(t) end
+
+  local t = f('Smile') --> t is `(Smile.Cat)?`
+  ```
+* `FIX` Fixed the error that the configuration file pointed to by the `--configpath` option was not read and loaded.
+* `FIX` Don't truncate any output when running in `--doc` mode [#3049](https://github.com/LuaLS/lua-language-server/issues/3049)
+* `FIX` Generic return can be optional.
+* `FIX` Fixed the comment calculating in docs `---@param a string?Comment` - now its `Comment` instead of `omment`.
+* `FIX` Fixed cannot bind variables using tail comment `@class` [#2673](https://github.com/LuaLS/lua-language-server/issues/2673)
+* `FIX` Fixed missing field completion for generic class object [#2196](https://github.com/LuaLS/lua-language-server/issues/2196) [#2945](https://github.com/LuaLS/lua-language-server/issues/2945) [#3041](https://github.com/LuaLS/lua-language-server/issues/3041)
 
 ## 3.13.5
 `2024-12-20`
