@@ -124,6 +124,8 @@ var CliClientOpenCommand = cli.Command{
124124
125125 SshPath : sshPath ,
126126 Debug : isDebug ,
127+
128+ TunneledPorts : make (map [string ]bool ),
127129 }
128130
129131 if nvrhContext .SshPath == "internal" {
@@ -469,6 +471,12 @@ func BuildClientNvimCmd(nvrhContext *context.NvrhContext) *exec.Cmd {
469471
470472func prepareRemoteNvim (nvrhContext * context.NvrhContext , nv * nvim.Nvim ) error {
471473 nv .RegisterHandler ("tunnel-port" , func (v * nvim.Nvim , args []string ) {
474+ if _ , ok := nvrhContext .TunneledPorts [args [0 ]]; ok {
475+ return
476+ }
477+
478+ nvrhContext .TunneledPorts [args [0 ]] = true
479+
472480 go nvrhContext .SshClient .TunnelSocket (& ssh_tunnel_info.SshTunnelInfo {
473481 Mode : "port" ,
474482 LocalSocket : args [0 ],
@@ -499,7 +507,6 @@ vim.api.nvim_create_user_command(
499507 force = true,
500508 }
501509)
502- return true
503510 ` , nil )
504511
505512 // Add command to open url.
@@ -532,10 +539,74 @@ script_contents = string.format(script_contents, socket_path, channel_id)
532539
533540vim.fn.writefile(vim.fn.split(script_contents, '\n'), browser_script_path)
534541os.execute('chmod +x ' .. browser_script_path)
535-
536- return true
537542 ` , nil , nvrhContext .BrowserScriptPath , nvrhContext .RemoteSocketOrPort (), nv .ChannelID ())
538543
544+ batch .ExecLua (`
545+ local nvrh_port_scanner = {
546+ active_watchers = {},
547+
548+ mapped_ports = {},
549+
550+ patterns = {
551+ -- "port 3000"
552+ "port%s+(%d+)",
553+ -- "localhost:3000"
554+ "localhost:(%d+)",
555+ -- "0.0.0.0:3000"
556+ "%d+%.%d+%.%d+%.%d+:(%d+)",
557+ -- ":3000" at start of line
558+ "^:(%d+)",
559+ -- ":3000" but avoid eslint errors (error in foo.tsx:3)
560+ "%s+:(%d+)",
561+ -- http://some.domain.com:3000 / https://some.domain.com:3000
562+ "https?://[^/]+:(%d+)",
563+ },
564+ }
565+
566+ function nvrh_port_scanner.attach_port_watcher(bufnr)
567+ if vim.bo[bufnr].buftype ~= "terminal" then
568+ return
569+ end
570+
571+ -- Already watching?
572+ if nvrh_port_scanner.active_watchers[bufnr] then
573+ return
574+ end
575+
576+ local function on_lines(_, _, _, lastline, new_lastline, _)
577+ local lines = vim.api.nvim_buf_get_lines(bufnr, lastline, new_lastline, false)
578+
579+ for _, line in ipairs(lines) do
580+ for _, pattern in ipairs(nvrh_port_scanner.patterns) do
581+ local port = string.match(line, pattern)
582+ if port then
583+ if nvrh_port_scanner.mapped_ports[port] then
584+ -- Already notified about this port
585+ else
586+ nvrh_port_scanner.mapped_ports[port] = true
587+ vim.rpcnotify(tonumber(os.getenv("NVRH_CHANNEL_ID")), "tunnel-port", { port })
588+ end
589+ break
590+ end
591+ end
592+ end
593+ end
594+
595+ vim.api.nvim_buf_attach(bufnr, false, {
596+ on_lines = on_lines,
597+ })
598+
599+ nvrh_port_scanner.active_watchers[bufnr] = true
600+ end
601+
602+ -- Attach watcher on TermOpen
603+ vim.api.nvim_create_autocmd("TermOpen", {
604+ callback = function(args)
605+ nvrh_port_scanner.attach_port_watcher(args.buf)
606+ end,
607+ })
608+ ` , nil )
609+
539610 batch .ExecLua (`
540611local original_open = vim.ui.open
541612vim.ui.open = function(uri, opts)
0 commit comments