fix/finder #2
@@ -1,6 +1,6 @@
|
|||||||
require('plugins.filetree')
|
require('plugins.filetree')
|
||||||
require('plugins.finder').setup({
|
require('plugins.finder').setup({
|
||||||
exclude_patterns = { 'node_modules', 'dist', 'build', '.git', '.cache', '.turbo' },
|
exclude_patterns = { 'node_modules', 'dist', 'build', '.git', '.cache', '.turbo', '*-lock.json' },
|
||||||
use_disk_cache = true, -- optional
|
use_disk_cache = true, -- optional
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ M.config = {
|
|||||||
grep_cmd = 'rg', -- ripgrep binary
|
grep_cmd = 'rg', -- ripgrep binary
|
||||||
page_size = 60, -- soft cap; real viewport height is measured
|
page_size = 60, -- soft cap; real viewport height is measured
|
||||||
debounce_ms = 80,
|
debounce_ms = 80,
|
||||||
|
instant_items = 1500, -- <= this count + cached => 0 ms debounce
|
||||||
|
fast_items = 4000, -- <= this count => debounce_ms/2
|
||||||
cache_ttl_sec = 20,
|
cache_ttl_sec = 20,
|
||||||
max_items = 5000, -- safety cap for massive outputs
|
max_items = 5000, -- safety cap for massive outputs
|
||||||
debug = false,
|
debug = false,
|
||||||
@@ -53,6 +55,9 @@ local S = {
|
|||||||
select = 1, -- absolute index in filtered (1-based)
|
select = 1, -- absolute index in filtered (1-based)
|
||||||
scroll = 0, -- top index (0-based)
|
scroll = 0, -- top index (0-based)
|
||||||
user_moved = false, -- has user navigated during this session?
|
user_moved = false, -- has user navigated during this session?
|
||||||
|
|
||||||
|
-- meta
|
||||||
|
files_cached = false,
|
||||||
}
|
}
|
||||||
|
|
||||||
---------------------------------------------------------------------
|
---------------------------------------------------------------------
|
||||||
@@ -81,34 +86,28 @@ local function cmd_exists(bin)
|
|||||||
return vim.fn.executable(bin) == 1
|
return vim.fn.executable(bin) == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- single reusable timer to avoid churn; caller controls ms
|
||||||
local function debounce(fn, ms)
|
local function debounce(fn, ms)
|
||||||
if S.timer then
|
if not S.timer then
|
||||||
S.timer:stop()
|
|
||||||
S.timer:close()
|
|
||||||
S.timer = nil
|
|
||||||
end
|
|
||||||
S.timer = vim.loop.new_timer()
|
S.timer = vim.loop.new_timer()
|
||||||
S.timer:start(ms, 0, function()
|
|
||||||
if S.timer then
|
|
||||||
S.timer:stop()
|
|
||||||
S.timer:close()
|
|
||||||
S.timer = nil
|
|
||||||
end
|
end
|
||||||
|
S.timer:stop()
|
||||||
|
S.timer:start(ms, 0, function()
|
||||||
|
S.timer:stop()
|
||||||
vim.schedule(fn)
|
vim.schedule(fn)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function read_gitignore(root)
|
local function read_gitignore(root)
|
||||||
local p = root .. '/.gitignore'
|
local p = root .. '/.gitignore'
|
||||||
local lines = {}
|
|
||||||
local f = io.open(p, 'r')
|
local f = io.open(p, 'r')
|
||||||
if not f then
|
if not f then
|
||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
|
local lines = {}
|
||||||
for line in f:lines() do
|
for line in f:lines() do
|
||||||
local l = vim.trim(line)
|
local l = vim.trim(line)
|
||||||
if #l > 0 and not l:match('^#') and not l:match('^!') then
|
if #l > 0 and not l:match('^#') and not l:match('^!') then
|
||||||
-- normalize directory suffix to plain token for our excludes
|
|
||||||
l = l:gsub('/+$', '')
|
l = l:gsub('/+$', '')
|
||||||
table.insert(lines, l)
|
table.insert(lines, l)
|
||||||
end
|
end
|
||||||
@@ -142,6 +141,7 @@ local function load_cache(root)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
if now_sec() - (data.timestamp or 0) > M.config.cache_ttl_sec then
|
if now_sec() - (data.timestamp or 0) > M.config.cache_ttl_sec then
|
||||||
|
pcall(os.remove, file) -- proactively clear stale
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
return data.files
|
return data.files
|
||||||
@@ -173,19 +173,24 @@ local function project_root()
|
|||||||
return vim.fn.getcwd(0, 0)
|
return vim.fn.getcwd(0, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Robust relative-to-root
|
||||||
local function normalize_rel(root, p)
|
local function normalize_rel(root, p)
|
||||||
if not p or p == '' then
|
if not p or p == '' then
|
||||||
return ''
|
return ''
|
||||||
end
|
end
|
||||||
-- strip leading ./ and leading root if any
|
local abs = vim.fs.normalize(vim.fn.fnamemodify(p, ':p'))
|
||||||
p = p:gsub('^%./', '')
|
|
||||||
local root_norm = vim.fs.normalize(root)
|
local root_norm = vim.fs.normalize(root)
|
||||||
local abs = vim.fs.normalize(p)
|
if abs == root_norm then
|
||||||
if abs:find(root_norm, 1, true) == 1 then
|
return ''
|
||||||
local rel = abs:sub(#root_norm + 2)
|
|
||||||
return rel ~= '' and rel or abs
|
|
||||||
end
|
end
|
||||||
return p
|
if abs:find(root_norm, 1, true) == 1 then
|
||||||
|
local ch = abs:sub(#root_norm + 1, #root_norm + 1)
|
||||||
|
if ch == '/' or ch == '\\' then
|
||||||
|
return abs:sub(#root_norm + 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- strip leading ./ as last resort
|
||||||
|
return (p:gsub('^%./', ''))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function to_abs_path(root, rel)
|
local function to_abs_path(root, rel)
|
||||||
@@ -195,6 +200,11 @@ local function to_abs_path(root, rel)
|
|||||||
return vim.fs.normalize(root .. '/' .. rel)
|
return vim.fs.normalize(root .. '/' .. rel)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function to_display_path(abs)
|
||||||
|
-- relative to current working dir if possible, else absolute
|
||||||
|
return vim.fn.fnamemodify(abs, ':.')
|
||||||
|
end
|
||||||
|
|
||||||
local function page_rows()
|
local function page_rows()
|
||||||
if S.win_res and vim.api.nvim_win_is_valid(S.win_res) then
|
if S.win_res and vim.api.nvim_win_is_valid(S.win_res) then
|
||||||
return math.max(1, vim.api.nvim_win_get_height(S.win_res))
|
return math.max(1, vim.api.nvim_win_get_height(S.win_res))
|
||||||
@@ -202,6 +212,18 @@ local function page_rows()
|
|||||||
return math.max(1, math.min(M.config.page_size, vim.o.lines))
|
return math.max(1, math.min(M.config.page_size, vim.o.lines))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function effective_debounce_ms()
|
||||||
|
if S.mode == 'files' then
|
||||||
|
local n = #S.items
|
||||||
|
if S.files_cached and n > 0 and n <= (M.config.instant_items or 1500) then
|
||||||
|
return 0
|
||||||
|
elseif n > 0 and n <= (M.config.fast_items or 4000) then
|
||||||
|
return math.max(0, math.floor((M.config.debounce_ms or 80) / 2))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return M.config.debounce_ms or 80
|
||||||
|
end
|
||||||
|
|
||||||
---------------------------------------------------------------------
|
---------------------------------------------------------------------
|
||||||
-- Render helpers
|
-- Render helpers
|
||||||
---------------------------------------------------------------------
|
---------------------------------------------------------------------
|
||||||
@@ -237,7 +259,7 @@ local function render()
|
|||||||
local end_idx = math.min(start_idx + page_rows() - 1, total)
|
local end_idx = math.min(start_idx + page_rows() - 1, total)
|
||||||
for i = start_idx, end_idx do
|
for i = start_idx, end_idx do
|
||||||
local line = S.filtered[i]
|
local line = S.filtered[i]
|
||||||
view[#view + 1] = type(line) == 'string' and line or tostring(line or '')
|
view[#view + 1] = tostring(line or '')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -283,7 +305,6 @@ end
|
|||||||
-- Querying / Filtering
|
-- Querying / Filtering
|
||||||
---------------------------------------------------------------------
|
---------------------------------------------------------------------
|
||||||
local function compute_positions_files(items, q)
|
local function compute_positions_files(items, q)
|
||||||
-- matchfuzzypos returns {items, positions}
|
|
||||||
local ok, res = pcall(vim.fn.matchfuzzypos, items, q)
|
local ok, res = pcall(vim.fn.matchfuzzypos, items, q)
|
||||||
if not ok or type(res) ~= 'table' then
|
if not ok or type(res) ~= 'table' then
|
||||||
return {}, {}
|
return {}, {}
|
||||||
@@ -293,14 +314,26 @@ local function compute_positions_files(items, q)
|
|||||||
|
|
||||||
local filtered, positions = {}, {}
|
local filtered, positions = {}, {}
|
||||||
for i, v in ipairs(out_items) do
|
for i, v in ipairs(out_items) do
|
||||||
filtered[i] = (type(v) == 'string') and v or tostring(v or '')
|
filtered[i] = tostring(v or '')
|
||||||
local cols = pos[i] or {}
|
local cols = pos[i] or {}
|
||||||
|
table.sort(cols)
|
||||||
local spans = {}
|
local spans = {}
|
||||||
|
local start_col, last_col = nil, nil
|
||||||
for _, c in ipairs(cols) do
|
for _, c in ipairs(cols) do
|
||||||
local start0 = (c > 0) and (c - 1) or 0 -- to 0-based
|
local c0 = c
|
||||||
spans[#spans + 1] = { start0, start0 + 1 }
|
if not start_col then
|
||||||
|
start_col, last_col = c0, c0
|
||||||
|
elseif c0 == last_col + 1 then
|
||||||
|
last_col = c0
|
||||||
|
else
|
||||||
|
table.insert(spans, { start_col, last_col + 1 }) -- end exclusive
|
||||||
|
start_col, last_col = c0, c0
|
||||||
end
|
end
|
||||||
positions[i] = spans
|
end
|
||||||
|
if start_col then
|
||||||
|
table.insert(spans, { start_col, last_col + 1 })
|
||||||
|
end
|
||||||
|
positions[i] = (#spans > 0) and spans or nil
|
||||||
end
|
end
|
||||||
return filtered, positions
|
return filtered, positions
|
||||||
end
|
end
|
||||||
@@ -312,15 +345,15 @@ local function compute_positions_grep(lines, q)
|
|||||||
local qlow = q:lower()
|
local qlow = q:lower()
|
||||||
local positions = {}
|
local positions = {}
|
||||||
for i, line in ipairs(lines) do
|
for i, line in ipairs(lines) do
|
||||||
local sidx = 1
|
local llow = tostring(line or ''):lower()
|
||||||
local spans = {}
|
local spans = {}
|
||||||
local llow = (type(line) == 'string' and line or tostring(line or '')):lower()
|
local sidx = 1
|
||||||
while true do
|
while true do
|
||||||
local s, e = string.find(llow, qlow, sidx, true)
|
local s, e = llow:find(qlow, sidx, true)
|
||||||
if not s then
|
if not s then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
spans[#spans + 1] = { s - 1, e } -- 0-based start, exclusive end
|
table.insert(spans, { s - 1, e })
|
||||||
sidx = e + 1
|
sidx = e + 1
|
||||||
if #spans > 64 then
|
if #spans > 64 then
|
||||||
break
|
break
|
||||||
@@ -346,10 +379,8 @@ local function set_items(list)
|
|||||||
|
|
||||||
-- sanitize to strings
|
-- sanitize to strings
|
||||||
for i, v in ipairs(list) do
|
for i, v in ipairs(list) do
|
||||||
if type(v) ~= 'string' then
|
|
||||||
list[i] = tostring(v or '')
|
list[i] = tostring(v or '')
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
S.items = list
|
S.items = list
|
||||||
S.filtered = list
|
S.filtered = list
|
||||||
@@ -379,16 +410,7 @@ local function set_query(q)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- sanitize filtered list
|
|
||||||
for i, v in ipairs(S.filtered) do
|
|
||||||
if type(v) ~= 'string' then
|
|
||||||
S.filtered[i] = tostring(v or '')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- selection policy:
|
-- selection policy:
|
||||||
-- if user hasn't moved, always reset to first on query changes;
|
|
||||||
-- if user moved, preserve previous pick when present, else first.
|
|
||||||
if not S.user_moved then
|
if not S.user_moved then
|
||||||
S.select = 1
|
S.select = 1
|
||||||
else
|
else
|
||||||
@@ -432,12 +454,12 @@ local function move_up()
|
|||||||
render()
|
render()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Correct parser for rg --vimgrep (file:line:col:match)
|
||||||
local function parse_vimgrep(line)
|
local function parse_vimgrep(line)
|
||||||
-- Greedy file capture up to last ":<lnum>:"
|
|
||||||
if type(line) ~= 'string' then
|
if type(line) ~= 'string' then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
local file, lnum = line:match('^(.*):(%d+):')
|
local file, lnum = line:match('^(.-):(%d+):%d+:')
|
||||||
if not file then
|
if not file then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -450,9 +472,10 @@ local function accept_selection_files()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local abs = to_abs_path(S.root, pick)
|
local abs = to_abs_path(S.root, pick)
|
||||||
|
local path = to_display_path(abs)
|
||||||
M.close()
|
M.close()
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
vim.cmd.edit(vim.fn.fnameescape(abs))
|
vim.cmd.edit(vim.fn.fnameescape(path))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -466,12 +489,12 @@ local function accept_selection_grep()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local abs = to_abs_path(S.root, file)
|
local abs = to_abs_path(S.root, file)
|
||||||
|
local path = to_display_path(abs)
|
||||||
local ln = tonumber(lnum) or 1
|
local ln = tonumber(lnum) or 1
|
||||||
M.close()
|
M.close()
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
vim.cmd.edit(vim.fn.fnameescape(abs))
|
vim.cmd.edit(vim.fn.fnameescape(path))
|
||||||
if pcall(vim.api.nvim_win_set_cursor, 0, { ln, 0 }) then
|
pcall(vim.api.nvim_win_set_cursor, 0, { ln, 0 })
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -493,9 +516,11 @@ local function collect_files_async(cb)
|
|||||||
|
|
||||||
local disk = load_cache(root)
|
local disk = load_cache(root)
|
||||||
if disk and type(disk) == 'table' then
|
if disk and type(disk) == 'table' then
|
||||||
|
S.files_cached = true
|
||||||
cb(disk)
|
cb(disk)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
S.files_cached = false
|
||||||
|
|
||||||
local file_cmd = M.config.file_cmd
|
local file_cmd = M.config.file_cmd
|
||||||
if not file_cmd then
|
if not file_cmd then
|
||||||
@@ -517,7 +542,6 @@ local function collect_files_async(cb)
|
|||||||
|
|
||||||
if file_cmd then
|
if file_cmd then
|
||||||
local args = { file_cmd, '--type', 'f', '--hidden', '--color', 'never' }
|
local args = { file_cmd, '--type', 'f', '--hidden', '--color', 'never' }
|
||||||
-- best-effort: avoid leading ./ in output
|
|
||||||
if file_cmd == 'fd' or file_cmd == 'fdfind' then
|
if file_cmd == 'fd' or file_cmd == 'fdfind' then
|
||||||
table.insert(args, '--strip-cwd-prefix')
|
table.insert(args, '--strip-cwd-prefix')
|
||||||
end
|
end
|
||||||
@@ -531,7 +555,7 @@ local function collect_files_async(cb)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local list = {}
|
local list = {}
|
||||||
if obj.code == 0 and obj.stdout then
|
if obj.stdout then
|
||||||
local raw = vim.split(obj.stdout, '\n', { trimempty = true })
|
local raw = vim.split(obj.stdout, '\n', { trimempty = true })
|
||||||
for _, p in ipairs(raw) do
|
for _, p in ipairs(raw) do
|
||||||
list[#list + 1] = normalize_rel(root, p)
|
list[#list + 1] = normalize_rel(root, p)
|
||||||
@@ -565,7 +589,7 @@ local function collect_files_async(cb)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local list = {}
|
local list = {}
|
||||||
if o2.code == 0 and o2.stdout then
|
if o2.stdout then
|
||||||
for p in o2.stdout:gmatch('([^%z]+)') do
|
for p in o2.stdout:gmatch('([^%z]+)') do
|
||||||
list[#list + 1] = normalize_rel(root, p)
|
list[#list + 1] = normalize_rel(root, p)
|
||||||
end
|
end
|
||||||
@@ -588,33 +612,8 @@ local function collect_files_async(cb)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Last resort: blocking glob with simple exclusion checks
|
-- Last resort omitted: blocking glob removed to keep async-only behavior
|
||||||
local list = vim.fn.globpath(root, '**/*', false, true)
|
cb({})
|
||||||
list = vim.tbl_filter(function(p)
|
|
||||||
if vim.fn.isdirectory(p) == 1 then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
for _, ex in ipairs(excludes) do
|
|
||||||
if p:find('/' .. ex .. '/', 1, true) or p:find('/' .. ex .. '$', 1, true) then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end, list)
|
|
||||||
|
|
||||||
local rel = {}
|
|
||||||
for i = 1, #list do
|
|
||||||
rel[i] = normalize_rel(root, list[i])
|
|
||||||
end
|
|
||||||
if #rel > M.config.max_items then
|
|
||||||
local tmp = {}
|
|
||||||
for i = 1, M.config.max_items do
|
|
||||||
tmp[i] = rel[i]
|
|
||||||
end
|
|
||||||
rel = tmp
|
|
||||||
end
|
|
||||||
save_cache(root, rel)
|
|
||||||
cb(rel)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function grep_async(query, cb)
|
local function grep_async(query, cb)
|
||||||
@@ -645,17 +644,13 @@ local function grep_async(query, cb)
|
|||||||
query,
|
query,
|
||||||
}
|
}
|
||||||
-- Apply excludes as negative globs
|
-- Apply excludes as negative globs
|
||||||
local excludes = {}
|
|
||||||
for _, p in ipairs(M.config.exclude_patterns or {}) do
|
for _, p in ipairs(M.config.exclude_patterns or {}) do
|
||||||
table.insert(excludes, p)
|
table.insert(args, 2, '--glob')
|
||||||
|
table.insert(args, 3, '!' .. p)
|
||||||
end
|
end
|
||||||
local gi = read_gitignore(root)
|
for _, p in ipairs(read_gitignore(root)) do
|
||||||
for _, p in ipairs(gi) do
|
table.insert(args, 2, '--glob')
|
||||||
table.insert(excludes, p)
|
table.insert(args, 3, '!' .. p)
|
||||||
end
|
|
||||||
for _, ex in ipairs(excludes) do
|
|
||||||
table.insert(args, 2, '--glob') -- insert before '--'
|
|
||||||
table.insert(args, 3, '!' .. ex)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
cancel_job(S.job_rg)
|
cancel_job(S.job_rg)
|
||||||
@@ -664,7 +659,8 @@ local function grep_async(query, cb)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local list = {}
|
local list = {}
|
||||||
if obj.code == 0 and obj.stdout then
|
-- Accept output even if exit code != 0 (no matches or partial errors)
|
||||||
|
if obj.stdout and #obj.stdout > 0 then
|
||||||
list = vim.split(obj.stdout, '\n', { trimempty = true })
|
list = vim.split(obj.stdout, '\n', { trimempty = true })
|
||||||
end
|
end
|
||||||
if #list > M.config.max_items then
|
if #list > M.config.max_items then
|
||||||
@@ -837,6 +833,7 @@ local function attach_handlers()
|
|||||||
group = S.aug,
|
group = S.aug,
|
||||||
buffer = S.buf_inp,
|
buffer = S.buf_inp,
|
||||||
callback = function()
|
callback = function()
|
||||||
|
local delay = effective_debounce_ms()
|
||||||
debounce(function()
|
debounce(function()
|
||||||
if not (S.active and S.buf_inp and vim.api.nvim_buf_is_valid(S.buf_inp)) then
|
if not (S.active and S.buf_inp and vim.api.nvim_buf_is_valid(S.buf_inp)) then
|
||||||
return
|
return
|
||||||
@@ -856,7 +853,7 @@ local function attach_handlers()
|
|||||||
else
|
else
|
||||||
set_query(q)
|
set_query(q)
|
||||||
end
|
end
|
||||||
end, M.config.debounce_ms)
|
end, delay)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
@@ -882,7 +879,7 @@ function M.files()
|
|||||||
if not S.active then
|
if not S.active then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
set_items(list) -- render initial list (relative paths)
|
set_items(list) -- render initial list (relative to root)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -907,8 +904,10 @@ function M.close()
|
|||||||
end
|
end
|
||||||
-- stop timers and jobs first
|
-- stop timers and jobs first
|
||||||
if S.timer then
|
if S.timer then
|
||||||
|
pcall(function()
|
||||||
S.timer:stop()
|
S.timer:stop()
|
||||||
S.timer:close()
|
S.timer:close()
|
||||||
|
end)
|
||||||
S.timer = nil
|
S.timer = nil
|
||||||
end
|
end
|
||||||
cancel_job(S.job_rg)
|
cancel_job(S.job_rg)
|
||||||
@@ -922,6 +921,7 @@ function M.close()
|
|||||||
S.items, S.filtered, S.positions = {}, {}, {}
|
S.items, S.filtered, S.positions = {}, {}, {}
|
||||||
S.query, S.select, S.scroll = '', 1, 0
|
S.query, S.select, S.scroll = '', 1, 0
|
||||||
S.user_moved = false
|
S.user_moved = false
|
||||||
|
S.files_cached = false
|
||||||
L('session closed')
|
L('session closed')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user