00a83ce4ac
Remove redundant code-view cancellation and align button labels. Restore module scrollbar thickness while preserving workspace colors, and support config, SDF and world text files locally and remotely.
70 lines
3.1 KiB
TypeScript
70 lines
3.1 KiB
TypeScript
type Request = { action: 'list' } | { action: 'content'; path: string } | { action: 'search'; query: string }
|
|
|
|
export function remoteModuleFilesScript(root: string, request: Request): string {
|
|
const input = Buffer.from(JSON.stringify({ root, ...request })).toString('base64')
|
|
return `python3 - '${input}' <<'PY'
|
|
import base64, json, os, stat, sys
|
|
args = json.loads(base64.b64decode(sys.argv[1]))
|
|
root = os.path.realpath(args['root'])
|
|
skip = {'.git', 'build', 'install', 'log', 'node_modules', 'docker'}
|
|
extensions = {'.c', '.cc', '.cpp', '.cxx', '.h', '.hpp', '.hh', '.py', '.xml', '.urdf', '.xacro', '.sdf', '.world', '.config', '.yaml', '.yml', '.json', '.txt', '.md', '.cmake', '.launch', '.sh', '.bash', '.toml', '.ini', '.cfg', '.rviz'}
|
|
placeholder = 'Binary or large file preview is not available.'
|
|
def content(relative):
|
|
if os.path.isabs(relative) or '..' in relative.split('/'):
|
|
raise ValueError('Invalid module path')
|
|
path = os.path.realpath(os.path.join(root, relative))
|
|
if os.path.commonpath([root, path]) != root:
|
|
raise ValueError('Invalid module path')
|
|
info = os.stat(path)
|
|
if not stat.S_ISREG(info.st_mode):
|
|
raise ValueError('Not a regular file')
|
|
if os.path.splitext(path)[1].lower() not in extensions or info.st_size > 1048576:
|
|
return placeholder
|
|
with open(path, 'rb') as fp:
|
|
data = fp.read(1048577)
|
|
if len(data) > 1048576 or b'\\x00' in data:
|
|
return placeholder
|
|
return data.decode('utf-8', errors='replace')
|
|
def entries():
|
|
def fail(error):
|
|
raise error
|
|
for current, dirs, files in os.walk(root, onerror=fail, followlinks=False):
|
|
depth = len(os.path.relpath(current, root).split('/')) if current != root else 0
|
|
dirs[:] = sorted(d for d in dirs if d not in skip and not os.path.islink(os.path.join(current, d)))
|
|
if depth >= 10:
|
|
dirs[:] = []
|
|
continue
|
|
for name in dirs:
|
|
yield 'D', os.path.relpath(os.path.join(current, name), root)
|
|
for name in sorted(files):
|
|
path = os.path.join(current, name)
|
|
if not os.path.islink(path) and os.path.isfile(path):
|
|
yield 'F', os.path.relpath(path, root)
|
|
if args['action'] == 'content':
|
|
print(json.dumps({'content': content(args['path'])}))
|
|
elif args['action'] == 'list':
|
|
paths, folders = [], []
|
|
for kind, path in entries():
|
|
(folders if kind == 'D' else paths).append(path)
|
|
print(json.dumps({'filePaths': paths, 'folders': folders}))
|
|
else:
|
|
query = args['query'].strip().lower()
|
|
results = []
|
|
if len(query) >= 2:
|
|
for kind, path in entries():
|
|
if kind != 'F':
|
|
continue
|
|
text = content(path)
|
|
if text == placeholder:
|
|
continue
|
|
for number, line in enumerate(text.split('\\n'), 1):
|
|
if query in line.lower():
|
|
results.append({'file': path, 'lineNumber': number, 'lineText': (line.strip() or line)[:2000]})
|
|
if len(results) >= 200:
|
|
break
|
|
if len(results) >= 200:
|
|
break
|
|
print(json.dumps(results))
|
|
PY`
|
|
}
|