mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	feat(lsp): workspace diagnostic support (#34262)
* refactor(lsp): remove underscore prefix from local variables * feat(lsp): workspace diagnostic support
This commit is contained in:
		 Maria José Solano
					Maria José Solano
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							d75ffa5934
						
					
				
				
					commit
					cb4559bc32
				
			| @@ -1855,6 +1855,18 @@ typehierarchy({kind})                            *vim.lsp.buf.typehierarchy()* | ||||
|     Parameters: ~ | ||||
|       • {kind}  (`"subtypes"|"supertypes"`) | ||||
|  | ||||
| workspace_diagnostics({opts})            *vim.lsp.buf.workspace_diagnostics()* | ||||
|     Request workspace-wide diagnostics. | ||||
|  | ||||
|     Parameters: ~ | ||||
|       • {opts}  (`table?`) A table with the following fields: | ||||
|                 • {client_id}? (`integer`) Only request diagnostics from the | ||||
|                   indicated client. If nil, the request is sent to all | ||||
|                   clients. | ||||
|  | ||||
|     See also: ~ | ||||
|       • https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_dagnostics | ||||
|  | ||||
| workspace_symbol({query}, {opts})             *vim.lsp.buf.workspace_symbol()* | ||||
|     Lists all symbols in the current workspace in the quickfix window. | ||||
|  | ||||
|   | ||||
| @@ -165,6 +165,8 @@ LSP | ||||
|   non-applicable LSP clients. | ||||
| • |vim.lsp.is_enabled()| checks if a LSP config is enabled (without | ||||
|   "resolving" it). | ||||
| • Support for `workspace/diagnostic`: |vim.lsp.buf.workspace_diagnostics()| | ||||
|   https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_dagnostics | ||||
|  | ||||
| LUA | ||||
|  | ||||
|   | ||||
| @@ -1015,6 +1015,21 @@ function M.workspace_symbol(query, opts) | ||||
|   request_with_opts(ms.workspace_symbol, params, opts) | ||||
| end | ||||
|  | ||||
| --- @class vim.lsp.WorkspaceDiagnosticsOpts | ||||
| --- @inlinedoc | ||||
| --- | ||||
| --- Only request diagnostics from the indicated client. If nil, the request is sent to all clients. | ||||
| --- @field client_id? integer | ||||
|  | ||||
| --- Request workspace-wide diagnostics. | ||||
| --- @param opts? vim.lsp.WorkspaceDiagnosticsOpts | ||||
| --- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_dagnostics | ||||
| function M.workspace_diagnostics(opts) | ||||
|   vim.validate('opts', opts, 'table', true) | ||||
|  | ||||
|   lsp.diagnostic._workspace_diagnostics(opts or {}) | ||||
| end | ||||
|  | ||||
| --- Send request to the server to resolve document highlights for the current | ||||
| --- text document position. This request can be triggered by a  key mapping or | ||||
| --- by events such as `CursorHold`, e.g.: | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| local protocol = require('vim.lsp.protocol') | ||||
| local lsp = vim.lsp | ||||
| local protocol = lsp.protocol | ||||
| local ms = protocol.Methods | ||||
| local util = vim.lsp.util | ||||
| local util = lsp.util | ||||
|  | ||||
| local api = vim.api | ||||
|  | ||||
| @@ -9,10 +10,10 @@ local M = {} | ||||
| local augroup = api.nvim_create_augroup('nvim.lsp.diagnostic', {}) | ||||
|  | ||||
| ---@class (private) vim.lsp.diagnostic.BufState | ||||
| ---@field enabled boolean Whether diagnostics are enabled for this buffer | ||||
| ---@field pull_kind 'document'|'workspace'|'disabled' Whether diagnostics are being updated via document pull, workspace pull, or disabled. | ||||
| ---@field client_result_id table<integer, string?> Latest responded `resultId` | ||||
|  | ||||
| ---@type table<integer,vim.lsp.diagnostic.BufState> | ||||
| ---@type table<integer, vim.lsp.diagnostic.BufState> | ||||
| local bufstates = {} | ||||
|  | ||||
| local DEFAULT_CLIENT_ID = -1 | ||||
| @@ -38,11 +39,11 @@ end | ||||
| ---@param bufnr integer | ||||
| ---@return string[]? | ||||
| local function get_buf_lines(bufnr) | ||||
|   if vim.api.nvim_buf_is_loaded(bufnr) then | ||||
|     return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) | ||||
|   if api.nvim_buf_is_loaded(bufnr) then | ||||
|     return api.nvim_buf_get_lines(bufnr, 0, -1, false) | ||||
|   end | ||||
|  | ||||
|   local filename = vim.api.nvim_buf_get_name(bufnr) | ||||
|   local filename = api.nvim_buf_get_name(bufnr) | ||||
|   local f = io.open(filename) | ||||
|   if not f then | ||||
|     return | ||||
| @@ -74,7 +75,7 @@ local function tags_lsp_to_vim(diagnostic, client_id) | ||||
|       tags = tags or {} | ||||
|       tags.deprecated = true | ||||
|     else | ||||
|       vim.lsp.log.info(string.format('Unknown DiagnosticTag %d from LSP client %d', tag, client_id)) | ||||
|       lsp.log.info(string.format('Unknown DiagnosticTag %d from LSP client %d', tag, client_id)) | ||||
|     end | ||||
|   end | ||||
|   return tags | ||||
| @@ -86,7 +87,7 @@ end | ||||
| ---@return vim.Diagnostic.Set[] | ||||
| local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id) | ||||
|   local buf_lines = get_buf_lines(bufnr) | ||||
|   local client = vim.lsp.get_client_by_id(client_id) | ||||
|   local client = lsp.get_client_by_id(client_id) | ||||
|   local position_encoding = client and client.offset_encoding or 'utf-16' | ||||
|   --- @param diagnostic lsp.Diagnostic | ||||
|   --- @return vim.Diagnostic.Set | ||||
| @@ -97,7 +98,7 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id) | ||||
|     if type(message) ~= 'string' then | ||||
|       vim.notify_once( | ||||
|         string.format('Unsupported Markup message from LSP client %d', client_id), | ||||
|         vim.lsp.log_levels.ERROR | ||||
|         lsp.log_levels.ERROR | ||||
|       ) | ||||
|       --- @diagnostic disable-next-line: undefined-field,no-unknown | ||||
|       message = diagnostic.message.value | ||||
| @@ -174,10 +175,10 @@ function M.from(diagnostics) | ||||
| end | ||||
|  | ||||
| ---@type table<integer, integer> | ||||
| local _client_push_namespaces = {} | ||||
| local client_push_namespaces = {} | ||||
|  | ||||
| ---@type table<string, integer> | ||||
| local _client_pull_namespaces = {} | ||||
| local client_pull_namespaces = {} | ||||
|  | ||||
| --- Get the diagnostic namespace associated with an LSP client |vim.diagnostic| for diagnostics | ||||
| --- | ||||
| @@ -186,7 +187,7 @@ local _client_pull_namespaces = {} | ||||
| function M.get_namespace(client_id, is_pull) | ||||
|   vim.validate('client_id', client_id, 'number') | ||||
|  | ||||
|   local client = vim.lsp.get_client_by_id(client_id) | ||||
|   local client = lsp.get_client_by_id(client_id) | ||||
|   if is_pull then | ||||
|     local server_id = | ||||
|       vim.tbl_get((client or {}).server_capabilities or {}, 'diagnosticProvider', 'identifier') | ||||
| @@ -196,19 +197,19 @@ function M.get_namespace(client_id, is_pull) | ||||
|       client_id, | ||||
|       server_id or 'nil' | ||||
|     ) | ||||
|     local ns = _client_pull_namespaces[key] | ||||
|     local ns = client_pull_namespaces[key] | ||||
|     if not ns then | ||||
|       ns = api.nvim_create_namespace(name) | ||||
|       _client_pull_namespaces[key] = ns | ||||
|       client_pull_namespaces[key] = ns | ||||
|     end | ||||
|     return ns | ||||
|   end | ||||
|  | ||||
|   local ns = _client_push_namespaces[client_id] | ||||
|   local ns = client_push_namespaces[client_id] | ||||
|   if not ns then | ||||
|     local name = ('nvim.lsp.%s.%d'):format(client and client.name or 'unknown', client_id) | ||||
|     ns = api.nvim_create_namespace(name) | ||||
|     _client_push_namespaces[client_id] = ns | ||||
|     client_push_namespaces[client_id] = ns | ||||
|   end | ||||
|   return ns | ||||
| end | ||||
| @@ -257,7 +258,7 @@ end | ||||
| function M.on_diagnostic(error, result, ctx) | ||||
|   if error ~= nil and error.code == protocol.ErrorCodes.ServerCancelled then | ||||
|     if error.data == nil or error.data.retriggerRequest ~= false then | ||||
|       local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) | ||||
|       local client = assert(lsp.get_client_by_id(ctx.client_id)) | ||||
|       client:request(ctx.method, ctx.params) | ||||
|     end | ||||
|     return | ||||
| @@ -271,7 +272,7 @@ function M.on_diagnostic(error, result, ctx) | ||||
|   handle_diagnostics(ctx.params.textDocument.uri, client_id, result.items, true) | ||||
|  | ||||
|   local bufnr = assert(ctx.bufnr) | ||||
|   local bufstate = assert(bufstates[bufnr]) | ||||
|   local bufstate = bufstates[bufnr] | ||||
|   bufstate.client_result_id[client_id] = result.resultId | ||||
| end | ||||
|  | ||||
| @@ -329,7 +330,7 @@ end | ||||
|  | ||||
| --- Clear diagnostics from pull based clients | ||||
| local function clear(bufnr) | ||||
|   for _, namespace in pairs(_client_pull_namespaces) do | ||||
|   for _, namespace in pairs(client_pull_namespaces) do | ||||
|     vim.diagnostic.reset(namespace, bufnr) | ||||
|   end | ||||
| end | ||||
| @@ -339,7 +340,7 @@ end | ||||
| local function disable(bufnr) | ||||
|   local bufstate = bufstates[bufnr] | ||||
|   if bufstate then | ||||
|     bufstate.enabled = false | ||||
|     bufstate.pull_kind = 'disabled' | ||||
|   end | ||||
|   clear(bufnr) | ||||
| end | ||||
| @@ -348,7 +349,7 @@ end | ||||
| ---@param bufnr integer buffer number | ||||
| ---@param client_id? integer Client ID to refresh (default: all clients) | ||||
| ---@param only_visible? boolean Whether to only refresh for the visible regions of the buffer (default: false) | ||||
| local function _refresh(bufnr, client_id, only_visible) | ||||
| local function refresh(bufnr, client_id, only_visible) | ||||
|   if | ||||
|     only_visible | ||||
|     and vim.iter(api.nvim_list_wins()):all(function(window) | ||||
| @@ -359,8 +360,8 @@ local function _refresh(bufnr, client_id, only_visible) | ||||
|   end | ||||
|  | ||||
|   local method = ms.textDocument_diagnostic | ||||
|   local clients = vim.lsp.get_clients({ bufnr = bufnr, method = method, id = client_id }) | ||||
|   local bufstate = assert(bufstates[bufnr]) | ||||
|   local clients = lsp.get_clients({ bufnr = bufnr, method = method, id = client_id }) | ||||
|   local bufstate = bufstates[bufnr] | ||||
|  | ||||
|   util._cancel_requests({ | ||||
|     bufnr = bufnr, | ||||
| @@ -383,54 +384,130 @@ end | ||||
| function M._enable(bufnr) | ||||
|   bufnr = vim._resolve_bufnr(bufnr) | ||||
|  | ||||
|   if not bufstates[bufnr] then | ||||
|     bufstates[bufnr] = { enabled = true, client_result_id = {} } | ||||
|  | ||||
|     api.nvim_create_autocmd('LspNotify', { | ||||
|       buffer = bufnr, | ||||
|       callback = function(opts) | ||||
|         if | ||||
|           opts.data.method ~= ms.textDocument_didChange | ||||
|           and opts.data.method ~= ms.textDocument_didOpen | ||||
|         then | ||||
|           return | ||||
|         end | ||||
|         if bufstates[bufnr] and bufstates[bufnr].enabled then | ||||
|           local client_id = opts.data.client_id --- @type integer? | ||||
|           _refresh(bufnr, client_id, true) | ||||
|         end | ||||
|       end, | ||||
|       group = augroup, | ||||
|     }) | ||||
|  | ||||
|     api.nvim_buf_attach(bufnr, false, { | ||||
|       on_reload = function() | ||||
|         if bufstates[bufnr] and bufstates[bufnr].enabled then | ||||
|           _refresh(bufnr) | ||||
|         end | ||||
|       end, | ||||
|       on_detach = function() | ||||
|         disable(bufnr) | ||||
|       end, | ||||
|     }) | ||||
|  | ||||
|     api.nvim_create_autocmd('LspDetach', { | ||||
|       buffer = bufnr, | ||||
|       callback = function(args) | ||||
|         local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_diagnostic }) | ||||
|  | ||||
|         if | ||||
|           not vim.iter(clients):any(function(c) | ||||
|             return c.id ~= args.data.client_id | ||||
|           end) | ||||
|         then | ||||
|           disable(bufnr) | ||||
|         end | ||||
|       end, | ||||
|       group = augroup, | ||||
|     }) | ||||
|   if bufstates[bufnr] then | ||||
|     -- If we're already pulling diagnostics for this buffer, nothing to do here. | ||||
|     if bufstates[bufnr].pull_kind == 'document' then | ||||
|       return | ||||
|     end | ||||
|     -- Else diagnostics were disabled or we were using workspace diagnostics. | ||||
|     bufstates[bufnr].pull_kind = 'document' | ||||
|   else | ||||
|     bufstates[bufnr].enabled = true | ||||
|     bufstates[bufnr] = { pull_kind = 'document', client_result_id = {} } | ||||
|   end | ||||
|  | ||||
|   api.nvim_create_autocmd('LspNotify', { | ||||
|     buffer = bufnr, | ||||
|     callback = function(opts) | ||||
|       if | ||||
|         opts.data.method ~= ms.textDocument_didChange | ||||
|         and opts.data.method ~= ms.textDocument_didOpen | ||||
|       then | ||||
|         return | ||||
|       end | ||||
|       if bufstates[bufnr] and bufstates[bufnr].pull_kind == 'document' then | ||||
|         local client_id = opts.data.client_id --- @type integer? | ||||
|         refresh(bufnr, client_id, true) | ||||
|       end | ||||
|     end, | ||||
|     group = augroup, | ||||
|   }) | ||||
|  | ||||
|   api.nvim_buf_attach(bufnr, false, { | ||||
|     on_reload = function() | ||||
|       if bufstates[bufnr] and bufstates[bufnr].pull_kind == 'document' then | ||||
|         refresh(bufnr) | ||||
|       end | ||||
|     end, | ||||
|     on_detach = function() | ||||
|       disable(bufnr) | ||||
|     end, | ||||
|   }) | ||||
|  | ||||
|   api.nvim_create_autocmd('LspDetach', { | ||||
|     buffer = bufnr, | ||||
|     callback = function(args) | ||||
|       local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_diagnostic }) | ||||
|  | ||||
|       if | ||||
|         not vim.iter(clients):any(function(c) | ||||
|           return c.id ~= args.data.client_id | ||||
|         end) | ||||
|       then | ||||
|         disable(bufnr) | ||||
|       end | ||||
|     end, | ||||
|     group = augroup, | ||||
|   }) | ||||
| end | ||||
|  | ||||
| --- Returns the result IDs from the reports provided by the given client. | ||||
| --- @return lsp.PreviousResultId[] | ||||
| local function previous_result_ids(client_id) | ||||
|   local results = {} | ||||
|  | ||||
|   for bufnr, state in pairs(bufstates) do | ||||
|     if state.pull_kind ~= 'disabled' then | ||||
|       for buf_client_id, result_id in pairs(state.client_result_id) do | ||||
|         if buf_client_id == client_id then | ||||
|           table.insert(results, { | ||||
|             textDocument = util.make_text_document_params(bufnr), | ||||
|             previousResultId = result_id, | ||||
|           }) | ||||
|           break | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   return results | ||||
| end | ||||
|  | ||||
| --- Request workspace-wide diagnostics. | ||||
| --- @param opts vim.lsp.WorkspaceDiagnosticsOpts | ||||
| function M._workspace_diagnostics(opts) | ||||
|   local clients = lsp.get_clients({ method = ms.workspace_diagnostic, id = opts.client_id }) | ||||
|  | ||||
|   --- @param error lsp.ResponseError? | ||||
|   --- @param result lsp.WorkspaceDiagnosticReport | ||||
|   --- @param ctx lsp.HandlerContext | ||||
|   local function handler(error, result, ctx) | ||||
|     -- Check for retrigger requests on cancellation errors. | ||||
|     -- Unless `retriggerRequest` is explicitly disabled, try again. | ||||
|     if error ~= nil and error.code == lsp.protocol.ErrorCodes.ServerCancelled then | ||||
|       if error.data == nil or error.data.retriggerRequest ~= false then | ||||
|         local client = assert(lsp.get_client_by_id(ctx.client_id)) | ||||
|         client:request(ms.workspace_diagnostic, ctx.params, handler) | ||||
|       end | ||||
|       return | ||||
|     end | ||||
|  | ||||
|     if error == nil and result ~= nil then | ||||
|       for _, report in ipairs(result.items) do | ||||
|         local bufnr = vim.uri_to_bufnr(report.uri) | ||||
|  | ||||
|         -- Start tracking the buffer (but don't send "textDocument/diagnostic" requests for it). | ||||
|         if not bufstates[bufnr] then | ||||
|           bufstates[bufnr] = { pull_kind = 'workspace', client_result_id = {} } | ||||
|         end | ||||
|  | ||||
|         -- We favor document pull requests over workspace results, so only update the buffer | ||||
|         -- state if we're not pulling document diagnostics for this buffer. | ||||
|         if bufstates[bufnr].pull_kind == 'workspace' and report.kind == 'full' then | ||||
|           handle_diagnostics(report.uri, ctx.client_id, report.items, true) | ||||
|           bufstates[bufnr].client_result_id[ctx.client_id] = report.resultId | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   for _, client in ipairs(clients) do | ||||
|     --- @type lsp.WorkspaceDiagnosticParams | ||||
|     local params = { | ||||
|       identifier = vim.tbl_get(client, 'server_capabilities, diagnosticProvider', 'identifier'), | ||||
|       previousResultIds = previous_result_ids(client.id), | ||||
|     } | ||||
|  | ||||
|     client:request(ms.workspace_diagnostic, params, handler) | ||||
|   end | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -566,6 +566,9 @@ function protocol.make_client_capabilities() | ||||
|       inlayHint = { | ||||
|         refreshSupport = true, | ||||
|       }, | ||||
|       workspace = { | ||||
|         refreshSupport = false, | ||||
|       }, | ||||
|     }, | ||||
|     experimental = nil, | ||||
|     window = { | ||||
|   | ||||
| @@ -6801,4 +6801,128 @@ describe('LSP', function() | ||||
|       eq(false, exec_lua([[return vim.lsp.is_enabled('foo')]])) | ||||
|     end) | ||||
|   end) | ||||
|  | ||||
|   describe('vim.lsp.buf.workspace_diagnostics()', function() | ||||
|     local fake_uri = 'file:///fake/uri' | ||||
|  | ||||
|     --- @param kind lsp.DocumentDiagnosticReportKind | ||||
|     --- @param msg string | ||||
|     --- @param pos integer | ||||
|     --- @return lsp.WorkspaceDocumentDiagnosticReport | ||||
|     local function make_report(kind, msg, pos) | ||||
|       return { | ||||
|         kind = kind, | ||||
|         uri = fake_uri, | ||||
|         items = { | ||||
|           { | ||||
|             range = { | ||||
|               start = { line = pos, character = pos }, | ||||
|               ['end'] = { line = pos, character = pos }, | ||||
|             }, | ||||
|             message = msg, | ||||
|             severity = 1, | ||||
|           }, | ||||
|         }, | ||||
|       } | ||||
|     end | ||||
|  | ||||
|     --- @param items lsp.WorkspaceDocumentDiagnosticReport[] | ||||
|     --- @return integer | ||||
|     local function setup_server(items) | ||||
|       exec_lua(create_server_definition) | ||||
|       return exec_lua(function() | ||||
|         _G.server = _G._create_server({ | ||||
|           capabilities = { | ||||
|             diagnosticProvider = { workspaceDiagnostics = true }, | ||||
|           }, | ||||
|           handlers = { | ||||
|             ['workspace/diagnostic'] = function(_, _, callback) | ||||
|               callback(nil, { items = items }) | ||||
|             end, | ||||
|           }, | ||||
|         }) | ||||
|         local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })) | ||||
|         vim.lsp.buf.workspace_diagnostics() | ||||
|         return client_id | ||||
|       end, { items }) | ||||
|     end | ||||
|  | ||||
|     it('updates diagnostics obtained with vim.diagnostic.get()', function() | ||||
|       setup_server({ make_report('full', 'Error here', 1) }) | ||||
|  | ||||
|       retry(nil, nil, function() | ||||
|         eq( | ||||
|           1, | ||||
|           exec_lua(function() | ||||
|             return #vim.diagnostic.get() | ||||
|           end) | ||||
|         ) | ||||
|       end) | ||||
|  | ||||
|       eq( | ||||
|         'Error here', | ||||
|         exec_lua(function() | ||||
|           return vim.diagnostic.get()[1].message | ||||
|         end) | ||||
|       ) | ||||
|     end) | ||||
|  | ||||
|     it('ignores unchanged diagnostic reports', function() | ||||
|       setup_server({ make_report('unchanged', '', 1) }) | ||||
|  | ||||
|       eq( | ||||
|         0, | ||||
|         exec_lua(function() | ||||
|           -- Wait for diagnostics to be processed. | ||||
|           vim.uv.sleep(50) | ||||
|  | ||||
|           return #vim.diagnostic.get() | ||||
|         end) | ||||
|       ) | ||||
|     end) | ||||
|  | ||||
|     it('favors document diagnostics over workspace diagnostics', function() | ||||
|       local client_id = setup_server({ make_report('full', 'Workspace error', 1) }) | ||||
|       local diagnostic_bufnr = exec_lua(function() | ||||
|         return vim.uri_to_bufnr(fake_uri) | ||||
|       end) | ||||
|  | ||||
|       exec_lua(function() | ||||
|         vim.lsp.diagnostic.on_diagnostic(nil, { | ||||
|           kind = 'full', | ||||
|           items = { | ||||
|             { | ||||
|               range = { | ||||
|                 start = { line = 2, character = 2 }, | ||||
|                 ['end'] = { line = 2, character = 2 }, | ||||
|               }, | ||||
|               message = 'Document error', | ||||
|               severity = 1, | ||||
|             }, | ||||
|           }, | ||||
|         }, { | ||||
|           method = 'textDocument/diagnostic', | ||||
|           params = { | ||||
|             textDocument = { uri = fake_uri }, | ||||
|           }, | ||||
|           client_id = client_id, | ||||
|           bufnr = diagnostic_bufnr, | ||||
|         }) | ||||
|       end) | ||||
|  | ||||
|       eq( | ||||
|         1, | ||||
|         exec_lua(function() | ||||
|           return #vim.diagnostic.get(diagnostic_bufnr) | ||||
|         end) | ||||
|       ) | ||||
|  | ||||
|       eq( | ||||
|         'Document error', | ||||
|         exec_lua(function() | ||||
|           return vim.diagnostic.get(vim.uri_to_bufnr(fake_uri))[1].message | ||||
|         end) | ||||
|       ) | ||||
|     end) | ||||
|   end) | ||||
| end) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user