diff --git a/lua/gitsigns/actions.lua b/lua/gitsigns/actions.lua index 49a6b70e9..cd4ca766e 100644 --- a/lua/gitsigns/actions.lua +++ b/lua/gitsigns/actions.lua @@ -485,6 +485,16 @@ M.blame_line = void(function(full) vim.list_extend(lines, commit_message) if full then + local prs = bcache.git_obj:associated_prs(result.sha) + if #prs > 0 then + lines[#lines + 1] = '' + lines[#lines + 1] = 'Associated pull requests:' + add_highlight('Title') + for _, pr in ipairs(prs) do + lines[#lines + 1] = string.format('#%d: %s', pr.id, pr.title) + end + end + hunk, ihunk, nhunk = get_blame_hunk(bcache.git_obj, result) end else @@ -547,8 +557,10 @@ M.diffthis = void(function(base) local comp_obj = bcache:get_compare_obj(calc_base(base)) if base then text, err = bcache.git_obj:get_show_text(comp_obj) - if err then - print(err) + for _, l in ipairs(err) do + print(l) + end + if #err > 0 then return end scheduler() diff --git a/lua/gitsigns/diff_ext.lua b/lua/gitsigns/diff_ext.lua index 247a605c2..4f1e9b106 100644 --- a/lua/gitsigns/diff_ext.lua +++ b/lua/gitsigns/diff_ext.lua @@ -56,7 +56,7 @@ M.run_diff = function( - git.command({ + local out = git.command({ '-c', 'core.safecrlf=false', 'diff', '--color=never', @@ -65,15 +65,16 @@ M.run_diff = function( '--unified=0', file_cmp, file_buf, - }, { - on_stdout = function(_, line) - if vim.startswith(line, '@@') then - table.insert(results, gs_hunks.parse_diff_line(line)) - elseif #results > 0 then - table.insert(results[#results].lines, line) - end - end, }) + + for _, line in ipairs(out) do + if vim.startswith(line, '@@') then + table.insert(results, gs_hunks.parse_diff_line(line)) + elseif #results > 0 then + table.insert(results[#results].lines, line) + end + end + os.remove(file_buf) os.remove(file_cmp) return results diff --git a/lua/gitsigns/git.lua b/lua/gitsigns/git.lua index c75aca603..8e93160eb 100644 --- a/lua/gitsigns/git.lua +++ b/lua/gitsigns/git.lua @@ -1,6 +1,5 @@ local wrap = require('plenary.async.async').wrap local scheduler = require('plenary.async.util').scheduler -local JobSpec = require('plenary.job').JobSpec local gsd = require("gitsigns.debug") local util = require('gitsigns.util') @@ -24,7 +23,13 @@ local GJobSpec = {} -local M = {BlameInfo = {}, Version = {}, Obj = {}, } +local M = {BlameInfo = {}, Version = {}, PrInfo = {}, Obj = {}, } + + + + + + @@ -121,41 +126,29 @@ local function check_version(version) end M.command = wrap(function(args, spec, callback) - local result = {} - local reserr spec = spec or {} spec.command = spec.command or 'git' spec.args = { '--no-pager', unpack(args) } - spec.on_stdout = spec.on_stdout or function(_, line) - table.insert(result, line) - if gsd.verbose and #result <= 10 then - gsd.vprint(line) - end - end - spec.on_stderr = spec.on_stderr or function(err, line) + local obj + obj = util.run_job(spec, function(_, _, stdout, stderr) if not spec.supress_stderr then - if err then gsd.eprint(err) end - if line then gsd.eprint(line) end - end - if not reserr then - reserr = '' - else - reserr = reserr .. '\n' - end - if err then reserr = reserr .. err end - if line then reserr = reserr .. line end - end - local old_on_exit = spec.on_exit - spec.on_exit = function() - if old_on_exit then - old_on_exit() + if #stderr > 0 then + print(vim.inspect(obj)) + print(vim.inspect(stderr)) + for _, l in ipairs(stderr) do + gsd.eprint(l) + end + end end - if gsd.verbose and #result then - gsd.vprintf('%d lines', #result) + + if gsd.verbose and #stdout <= 10 then + for _, l in ipairs(stdout) do + gsd.vprint(l) + end end - callback(result, reserr) - end - util.run_job(spec) + + callback(stdout, stderr) + end) end, 3) local function process_abbrev_head(gitdir, head_str, path, cmd) @@ -282,9 +275,7 @@ end Obj.get_show_text = function(self, object) - return self:command({ 'show', object }, { - supress_stderr = true, - }) + return self:command({ 'show', object }, { supress_stderr = true }) end Obj.run_blame = function(self, lines, lnum) @@ -375,14 +366,110 @@ Obj.has_moved = function(self) end Obj.files_changed = function(self) + local results = self:command({ 'status', '--porcelain' }) + local ret = {} - self:command({ 'status', '--porcelain' }, { - on_stdout = function(_, line) - if line:sub(1, 2):match('^.M') then - ret[#ret + 1] = line:sub(4, -1) - end - end, + for _, line in ipairs(results) do + if line:sub(1, 2):match('^.M') then + ret[#ret + 1] = line:sub(4, -1) + end + end + return ret +end + +local raw_cmd = wrap(function(opts, callback) + local stdout = uv.new_pipe(false) + local stderr = uv.new_pipe(false) + opts.stdio = { nil, stdout, stderr } + + local stdout_chunks = {} + local stderr_chunks = {} + + local handle = uv.spawn(opts.cmd, opts, function(_, _) + if stdout and not stdout:is_closing() then stdout:close() end + if stderr and not stderr:is_closing() then stderr:close() end + callback( + vim.split(table.concat(stdout_chunks), '\n'), + vim.split(table.concat(stderr_chunks), '\n')) + + end) + + if not handle then + error(debug.traceback("Failed to spawn process: " .. vim.inspect(opts))) + end + + stdout:read_start(function(_, data) + stdout_chunks[#stdout_chunks + 1] = data + end) + + stderr:read_start(function(_, data) + stderr_chunks[#stderr_chunks + 1] = data + end) + +end, 2) + +local associated_pr_query = [[ + query associatedPRs($sha: String!, $repo: String!, $owner: String!){ + repository(name: $repo, owner: $owner) { + commit: object(expression: $sha) { + ... on Commit { + associatedPullRequests(first:5){ + edges { node { title number } } + } + } + } + } + } +]] + +local associated_pr_template = [[ + {{- if .data.repository.commit -}} + {{- range $edge := .data.repository.commit.associatedPullRequests.edges -}} + {{- printf "%.f %s" $edge.node.number $edge.node.title -}} + {{- end -}} + {{- end -}} +]] + +Obj.associated_prs = function(self, sha) + local ret = {} + + local result = raw_cmd({ + cmd = 'gh', + cwd = self.toplevel, + args = { + 'dwqapi', 'graphql', + '-f', 'query=' .. associated_pr_query, + '--template=' .. associated_pr_template, + '-F', "owner={owner}", + '-F', "repo={repo}", + '-F', 'sha=' .. sha, + '--paginate', }, }) + + + + + + + + + + + + + + + + + print('DEBUG487') + for _, line in ipairs(result) do + print('DEBUG489: ' .. line) + local id, title = line:match('(%d+) (.*)') + if id and title then + ret[#ret + 1] = { id = tonumber(id), title = title } + end + end + return ret end diff --git a/lua/gitsigns/util.lua b/lua/gitsigns/util.lua index 9d00fcc8f..f832193a7 100644 --- a/lua/gitsigns/util.lua +++ b/lua/gitsigns/util.lua @@ -1,8 +1,28 @@ -local Job = require('plenary.job') - local gsd = require("gitsigns.debug") +local uv = vim.loop + +local M = {JobSpec = {State = {}, }, } + + + + + + + + + + + + + + + + + + + + -local M = {} @@ -11,16 +31,99 @@ local M = {} M.job_cnt = 0 function M.path_exists(path) - return vim.loop.fs_stat(path) and true or false + return uv.fs_stat(path) and true or false end -function M.run_job(job_spec) +function M.run_job(obj, callback) if gsd.debug_mode then - local cmd = job_spec.command .. ' ' .. table.concat(job_spec.args, ' ') + local cmd = obj.command .. ' ' .. table.concat(obj.args, ' ') gsd.dprint(cmd) end - Job:new(job_spec):start() - M.job_cnt = M.job_cnt + 1 + + if obj.env then + local transform = {} + for k, v in pairs(obj.env) do + if type(k) == "number" then + table.insert(transform, v) + elseif type(k) == "string" then + table.insert(transform, k .. "=" .. tostring(v)) + end + end + obj.env = transform + end + + obj._state = {} + local s = obj._state + s.stdout_data = {} + s.stderr_data = {} + + s.stdout = uv.new_pipe(false) + s.stderr = uv.new_pipe(false) + + s.handle, s.pid = uv.spawn(obj.command, { + args = obj.args, + stdio = { s.stdin, s.stdout, s.stderr }, + cwd = obj.cwd, + env = obj.env, + }, + function(code, signal) + s.code = code + s.signal = signal + + if s.stdout then s.stdout:read_stop() end + if s.stderr then s.stderr:read_stop() end + + for _, handle in ipairs({ s.stdin, s.stderr, s.stdout }) do + if handle and not handle:is_closing() then + handle:close() + end + end + + local stdout_result = s.stdout_data and vim.split(table.concat(s.stdout_data), '\n') + local stderr_result = s.stderr_data and vim.split(table.concat(s.stderr_data), '\n') + + callback(code, signal, stdout_result, stderr_result) + end) + + + if not s.handle then + error(debug.traceback("Failed to spawn process: " .. vim.inspect(obj))) + end + + s.stdout:read_start(function(_, data) + if not s.stdout_data then + s.stdout_data = {} + end + s.stdout_data[#s.stdout_data + 1] = data + end) + + s.stderr:read_start(function(_, data) + if not s.stderr_data then + s.stderr_data = {} + end + s.stderr_data[#s.stderr_data + 1] = data + end) + + if type(obj.writer) == "table" and vim.tbl_islist(obj.writer) then + local writer_table = obj.writer + local writer_len = #writer_table + for i, v in ipairs(writer_table) do + s.stdin:write(v) + if i ~= writer_len then + s.stdin:write("\n") + else + s.stdin:write("\n", function() + s.stdin:close() + end) + end + end + elseif type(obj.writer) == "string" then + s.stdin:write(obj.writer, function() + s.stdin:close() + end) + end + + return obj end function M.get_jit_os() diff --git a/teal/gitsigns/actions.tl b/teal/gitsigns/actions.tl index 460da9830..5f53fb01a 100644 --- a/teal/gitsigns/actions.tl +++ b/teal/gitsigns/actions.tl @@ -485,6 +485,16 @@ M.blame_line = void(function(full: boolean) vim.list_extend(lines, commit_message) if full then + local prs = bcache.git_obj:associated_prs(result.sha) + if #prs > 0 then + lines[#lines+1] = '' + lines[#lines+1] = 'Associated pull requests:' + add_highlight('Title') + for _, pr in ipairs(prs) do + lines[#lines+1] = string.format('#%d: %s', pr.id, pr.title) + end + end + hunk, ihunk, nhunk = get_blame_hunk(bcache.git_obj, result) end else @@ -543,12 +553,14 @@ M.diffthis = void(function(base: string) if api.nvim_win_get_option(0, 'diff') then return end local text: {string} - local err: string + local err: {string} local comp_obj = bcache:get_compare_obj(calc_base(base)) if base then text, err = bcache.git_obj:get_show_text(comp_obj) - if err then - print(err) + for _, l in ipairs(err) do + print(l) + end + if #err > 0 then return end scheduler() diff --git a/teal/gitsigns/diff_ext.tl b/teal/gitsigns/diff_ext.tl index 6a1fe9d23..bdd88922f 100644 --- a/teal/gitsigns/diff_ext.tl +++ b/teal/gitsigns/diff_ext.tl @@ -56,24 +56,25 @@ M.run_diff = function( -- We can safely ignore the warning, we turn it off by passing the '-c -- "core.safecrlf=false"' argument to git-diff. - git.command({ - '-c', 'core.safecrlf=false', - 'diff', - '--color=never', - '--diff-algorithm='..diff_algo, - '--patch-with-raw', - '--unified=0', - file_cmp, - file_buf, - }, { - on_stdout = function(_, line: string) - if vim.startswith(line, '@@') then - table.insert(results, gs_hunks.parse_diff_line(line)) - elseif #results > 0 then - table.insert(results[#results].lines, line) - end + local out = git.command{ + '-c', 'core.safecrlf=false', + 'diff', + '--color=never', + '--diff-algorithm='..diff_algo, + '--patch-with-raw', + '--unified=0', + file_cmp, + file_buf, + } + + for _, line in ipairs(out) do + if vim.startswith(line, '@@') then + table.insert(results, gs_hunks.parse_diff_line(line)) + elseif #results > 0 then + table.insert(results[#results].lines, line) end - }) + end + os.remove(file_buf) os.remove(file_cmp) return results diff --git a/teal/gitsigns/git.tl b/teal/gitsigns/git.tl index 521a30dfe..aa9a9ea45 100644 --- a/teal/gitsigns/git.tl +++ b/teal/gitsigns/git.tl @@ -1,6 +1,5 @@ local wrap = require('plenary.async.async').wrap local scheduler = require('plenary.async.util').scheduler -local JobSpec = require('plenary.job').JobSpec local gsd = require("gitsigns.debug") local util = require('gitsigns.util') @@ -58,7 +57,12 @@ local record M enable_yadm: boolean set_version: function(string) - command : function(args: {string}, spec: GJobSpec): {string}, string + command : function(args: {string}, spec: GJobSpec): {string}, {string} + + record PrInfo + id: integer + title: string + end record Obj toplevel : string @@ -72,17 +76,18 @@ local record M mode_bits : string has_conflicts : boolean - command : function(Obj, {string}, GJobSpec): {string}, string + command : function(Obj, {string}, GJobSpec): {string}, {string} update_abbrev_head : function(Obj) update_file_info : function(Obj): boolean unstage_file : function(Obj, string, string) - get_show_text : function(Obj, string): {string}, string - run_blame : function(Obj, {string}, number): M.BlameInfo + get_show_text : function(Obj, string): {string}, {string} + run_blame : function(Obj, {string}, number): BlameInfo file_info : function(Obj, string): string, string, string, boolean ensure_file_in_index : function(Obj) stage_hunks : function(Obj, {Hunk}, boolean) has_moved : function(Obj): string files_changed : function(Obj): {string} + associated_prs : function(Obj, string): {PrInfo} new : function(string): Obj end @@ -120,42 +125,30 @@ local function check_version(version: {number,number,number}): boolean return true end -M.command = wrap(function(args: {string}, spec: GJobSpec, callback: function({string}, string)) - local result: {string} = {} - local reserr: string +M.command = wrap(function(args: {string}, spec: GJobSpec, callback: function({string}, {string})) spec = spec or {} spec.command = spec.command or 'git' spec.args = {'--no-pager', unpack(args) } - spec.on_stdout = spec.on_stdout or function(_, line: string) - table.insert(result, line) - if gsd.verbose and #result <= 10 then - gsd.vprint(line) - end - end - spec.on_stderr = spec.on_stderr or function(err: string, line: string) + local obj: util.JobSpec + obj = util.run_job(spec as util.JobSpec, function(_: integer, _: integer, stdout: {string}, stderr: {string}) if not spec.supress_stderr then - if err then gsd.eprint(err) end - if line then gsd.eprint(line) end - end - if not reserr then - reserr = '' - else - reserr = reserr..'\n' - end - if err then reserr = reserr..err end - if line then reserr = reserr..line end - end - local old_on_exit = spec.on_exit - spec.on_exit = function() - if old_on_exit then - old_on_exit() + if #stderr > 0 then + print(vim.inspect(obj)) + print(vim.inspect(stderr)) + for _, l in ipairs(stderr) do + gsd.eprint(l) + end + end end - if gsd.verbose and #result then - gsd.vprintf('%d lines', #result) + + if gsd.verbose and #stdout <= 10 then + for _, l in ipairs(stdout) do + gsd.vprint(l) + end end - callback(result, reserr) - end - util.run_job(spec as JobSpec) + + callback(stdout, stderr) + end) end, 3) local function process_abbrev_head(gitdir: string, head_str: string, path: string, cmd: string): string @@ -226,7 +219,7 @@ end -------------------------------------------------------------------------------- --- Run git command the with the objects gitdir and toplevel -Obj.command = function(self: Obj, args: {string}, spec: GJobSpec): {string}, string +Obj.command = function(self: Obj, args: {string}, spec: GJobSpec): {string}, {string} spec = spec or {} spec.cwd = self.toplevel return M.command({'--git-dir='..self.gitdir, unpack(args)}, spec) @@ -281,10 +274,8 @@ Obj.unstage_file = function(self: Obj) end --- Get version of file in the index, return array lines -Obj.get_show_text = function(self: Obj, object: string): {string}, string - return self:command({'show', object}, { - supress_stderr = true - }) +Obj.get_show_text = function(self: Obj, object: string): {string}, {string} + return self:command({'show', object}, { supress_stderr = true }) end Obj.run_blame = function(self: Obj, lines: {string}, lnum: number): M.BlameInfo @@ -375,14 +366,110 @@ Obj.has_moved = function(self: Obj): string end Obj.files_changed = function(self: Obj): {string} + local results = self:command{'status', '--porcelain'} + local ret: {string} = {} - self:command({'status', '--porcelain'}, { - on_stdout = function(_, line: string) - if line:sub(1, 2):match('^.M') then - ret[#ret+1] = line:sub(4, -1) - end + for _, line in ipairs(results) do + if line:sub(1, 2):match('^.M') then + ret[#ret+1] = line:sub(4, -1) end - }) + end + return ret +end + +local raw_cmd = wrap(function(opts: {string:any}, callback: function({string}, {string})) + local stdout = uv.new_pipe(false) + local stderr = uv.new_pipe(false) + opts.stdio = {nil, stdout, stderr} + + local stdout_chunks: {string} = {} + local stderr_chunks: {string} = {} + + local handle = uv.spawn(opts.cmd as string, opts as uv.SpawnOpts, function(_: integer, _: integer) + if stdout and not stdout:is_closing() then stdout:close() end + if stderr and not stderr:is_closing() then stderr:close() end + callback( + vim.split(table.concat(stdout_chunks), '\n'), + vim.split(table.concat(stderr_chunks), '\n') + ) + end) + + if not handle then + error(debug.traceback("Failed to spawn process: " .. vim.inspect(opts))) + end + + stdout:read_start(function(_, data: string) + stdout_chunks[#stdout_chunks+1] = data + end) + + stderr:read_start(function(_, data: string) + stderr_chunks[#stderr_chunks+1] = data + end) + +end, 2) + +local associated_pr_query=[[ + query associatedPRs($sha: String!, $repo: String!, $owner: String!){ + repository(name: $repo, owner: $owner) { + commit: object(expression: $sha) { + ... on Commit { + associatedPullRequests(first:5){ + edges { node { title number } } + } + } + } + } + } +]] + +local associated_pr_template=[[ + {{- if .data.repository.commit -}} + {{- range $edge := .data.repository.commit.associatedPullRequests.edges -}} + {{- printf "%.f %s" $edge.node.number $edge.node.title -}} + {{- end -}} + {{- end -}} +]] + +Obj.associated_prs = function(self: Obj, sha: string): {M.PrInfo} + local ret: {M.PrInfo} = {} + + local result = raw_cmd { + cmd = 'gh', + cwd = self.toplevel, + args = { + 'dwqapi', 'graphql', + '-f', 'query='..associated_pr_query, + '--template='..associated_pr_template, + '-F', "owner={owner}", + '-F', "repo={repo}", + '-F', 'sha='..sha, + '--paginate'} + } + + -- aaa = true + -- local result = M.command({ + -- 'api', 'graphql', + -- '-f', 'query='..associated_pr_query, + -- '--template='..associated_pr_template, + -- '-F', "owner={owner}", + -- '-F', "repo={repo}", + -- '-F', 'sha='..sha, + -- '--paginate' + -- }, { + -- command = 'gh', + -- cwd = self.toplevel, + -- }) + -- aaa = false + + print('DEBUG487') + for _, line in ipairs(result) do + print('DEBUG489: '..line) + local id, title = line:match('(%d+) (.*)') + if id and title then + ret[#ret+1] = { id = tonumber(id) as integer, title = title } + end + end + return ret end diff --git a/teal/gitsigns/util.tl b/teal/gitsigns/util.tl index 369893393..30d6d1b65 100644 --- a/teal/gitsigns/util.tl +++ b/teal/gitsigns/util.tl @@ -1,26 +1,129 @@ -local Job = require('plenary.job') - local gsd = require("gitsigns.debug") +local uv = vim.loop local record M path_sep: string is_unix: boolean job_cnt: integer + + record JobSpec + command: string + args: {string} + cwd: string + env: {string} + writer: {string} | string + + record State + handle: uv.Process + pid: integer + stdout_data: {string} + stderr_data: {string} + stdin: uv.Pipe + stdout: uv.Pipe + stderr: uv.Pipe + code: integer + signal: integer + end + _state: State + end end M.job_cnt = 0 function M.path_exists(path: string): boolean - return vim.loop.fs_stat(path) and true or false + return uv.fs_stat(path) and true or false end -function M.run_job(job_spec: Job.JobSpec) +function M.run_job(obj: M.JobSpec, callback: function(integer, integer, {string}, {string})): M.JobSpec if gsd.debug_mode then - local cmd: string = job_spec.command..' '..table.concat(job_spec.args, ' ') + local cmd: string = obj.command..' '..table.concat(obj.args, ' ') gsd.dprint(cmd) end - Job:new(job_spec):start() - M.job_cnt = M.job_cnt + 1 + + if obj.env then + local transform: {string} = {} + for k, v in pairs(obj.env as {integer:string}) do + if type(k) == "number" then + table.insert(transform, v) + elseif type(k) == "string" then + table.insert(transform, k .. "=" .. tostring(v)) + end + end + obj.env = transform + end + + obj._state = {} + local s = obj._state + s.stdout_data = {} + s.stderr_data = {} + + s.stdout = uv.new_pipe(false) + s.stderr = uv.new_pipe(false) + + s.handle, s.pid = uv.spawn(obj.command, { + args = obj.args, + stdio = { s.stdin, s.stdout, s.stderr }, + cwd = obj.cwd, + env = obj.env + }, + function(code: integer, signal: integer) + s.code = code + s.signal = signal + + if s.stdout then s.stdout:read_stop() end + if s.stderr then s.stderr:read_stop() end + + for _, handle in ipairs {s.stdin, s.stderr, s.stdout} do + if handle and not handle:is_closing() then + handle:close() + end + end + + local stdout_result = s.stdout_data and vim.split(table.concat(s.stdout_data), '\n') + local stderr_result = s.stderr_data and vim.split(table.concat(s.stderr_data), '\n') + + callback(code, signal, stdout_result, stderr_result) + end + ) + + if not s.handle then + error(debug.traceback("Failed to spawn process: " .. vim.inspect(obj))) + end + + s.stdout:read_start(function(_, data: string) + if not s.stdout_data then + s.stdout_data = {} + end + s.stdout_data[#s.stdout_data+1] = data + end) + + s.stderr:read_start(function(_, data: string) + if not s.stderr_data then + s.stderr_data = {} + end + s.stderr_data[#s.stderr_data+1] = data + end) + + if type(obj.writer) == "table" and vim.tbl_islist(obj.writer as table) then + local writer_table = obj.writer as {string} + local writer_len = #writer_table + for i, v in ipairs(writer_table) do + s.stdin:write(v) + if i ~= writer_len then + s.stdin:write "\n" + else + s.stdin:write("\n", function() + s.stdin:close() + end) + end + end + elseif type(obj.writer) == "string" then + s.stdin:write(obj.writer as string, function() + s.stdin:close() + end) + end + + return obj end function M.get_jit_os(): string diff --git a/types/types.d.tl b/types/types.d.tl index 9e9c208a2..fe24ebe7a 100644 --- a/types/types.d.tl +++ b/types/types.d.tl @@ -312,6 +312,34 @@ global record vim new_tcp: function() sleep: function(integer) + + record Pipe + close: function(Pipe) + is_closing: function(Pipe): boolean | string + read_start: function(Pipe, err: any, data: string) + read_stop: function(Pipe) + write: function(Pipe, string, function()) + + open: function(any) + end + + record Process + userdata + end + + record SpawnOpts + stdio: {Pipe, Pipe, Pipe} + args: {string} + cwd: string + env: {string} + end + + spawn: function(string, SpawnOpts, function(integer, integer)): Process, integer + + read_start: function(Pipe, function) + new_pipe: function(boolean): Pipe + shutdown: function(any, function) + close: function(any, function) end in_fast_event: function(): boolean @@ -358,6 +386,8 @@ global record vim tbl_isempty: function(table): boolean + tbl_islist: function(table): boolean + tbl_contains: function(table, any): boolean record InspectOptions