From fc2c2f9f823d66ea38dd34e1eb934caa62c9f9f0 Mon Sep 17 00:00:00 2001 From: Ryan Livingston <43882944+rlivings39@users.noreply.github.com> Date: Sun, 14 Jun 2020 15:04:00 -0400 Subject: [PATCH] Add linux named pipe support --- .vscode/launch.json | 9 ++ CHANGELOG.md | 6 ++ package.json | 5 +- scripts/topipe.sh | 7 ++ src/extension.ts | 175 ++++++++++++++++++------------- src/test/suite/extension.test.ts | 2 +- 6 files changed, 127 insertions(+), 77 deletions(-) create mode 100755 scripts/topipe.sh diff --git a/.vscode/launch.json b/.vscode/launch.json index 9748cb4..4777d25 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,6 +5,15 @@ { "version": "0.2.0", "configurations": [ + { + "name": "Launch Program", + "program": "${workspaceFolder}/src/socketfun.ts", + "request": "launch", + "skipFiles": [ + "/**" + ], + "type": "pwa-node" + }, { "name": "Extension", "type": "extensionHost", diff --git a/CHANGELOG.md b/CHANGELOG.md index c641389..4bb1813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.3.0] - 2020-06-14 + +### Changed + +- Changed implementation to avoid using command line utilities: `xargs, cut, code`. `fzf` output is now sent through named pipes and processed in the VSCode extension code. This avoids issues with having those utilities installed, having the wrong `code` on the path, and allows for implemeting arbitrary functionality beyond what the `code` command supports. + ## [0.2.12] - 2020-05-26 ### Added diff --git a/package.json b/package.json index 8571438..b688bb1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fzf-quick-open", "displayName": "fzf fuzzy quick open", "description": "Fuzzy search files/folders and ripgrep results with fzf inside VSCode", - "version": "0.2.12", + "version": "0.3.0", "author": "Ryan Livingston", "license": "MIT", "repository": { @@ -71,6 +71,7 @@ "scope": "window", "type": "string", "default": "code", + "deprecationMessage": "DEPRECATED: This option has no utility as of v0.3.0. We avoid use of the code command line API and don't need this to distinguish code from code-insiders. Please report unexpected issues based on the removal of this property.", "description": "Command used to launch VSCode. Set to code-insiders if using insiders build" }, "fzf-quick-open.fuzzyCmd": { @@ -122,4 +123,4 @@ "typescript": "^3.8.3", "vscode-test": "^1.0.0" } -} +} \ No newline at end of file diff --git a/scripts/topipe.sh b/scripts/topipe.sh new file mode 100755 index 0000000..39ad740 --- /dev/null +++ b/scripts/topipe.sh @@ -0,0 +1,7 @@ +#! /usr/bin/env sh +# USAGE: topipe.bat commandString pipename +# reads input to send to pipename from input pipe +sep='$$' +read arg +# commandString{open, add, rg} pwd pipedInput > pipeName +echo "$1 $sep `pwd` $sep $arg" > $2 diff --git a/src/extension.ts b/src/extension.ts index d429874..e18ede9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,17 +4,19 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; import * as net from 'net'; +import * as os from 'os'; +import * as cp from 'child_process'; + let fzfTerminal: vscode.Terminal | undefined = undefined; let fzfTerminalPwd: vscode.Terminal | undefined = undefined; -let codeCmd: string; let codePath: string; let findCmd: string; let fzfCmd: string; let initialCwd: string; let rgCaseFlag: string; let fzfPipe: string; -let fzfPipeBatch: string; +let fzfPipeScript: string; export const TERMINAL_NAME = "fzf terminal"; export const TERMINAL_NAME_PWD = "fzf pwd terminal"; @@ -79,21 +81,6 @@ function isWindows() { return process.platform === 'win32'; } -function setupCodeCmd() { - codePath = vscode.env.appName.toLowerCase().indexOf('insiders') !== -1 ? 'code-insiders' : 'code'; - // Go find the code executable if we can. This adds stability for when the user's shell - // doesn't have the right code on the top of the path - let binRoot = vscode.env.appRoot; - let localPath = path.join(binRoot, '..', '..', 'bin'); - let remotePath = path.join(binRoot, 'bin'); - if (fs.existsSync(localPath)) { - codePath = path.join(localPath, codePath); - } else if (fs.existsSync(remotePath)) { - codePath = path.join(remotePath, codePath); - } - codeCmd = `"${codePath}"`; -} - function getPath(arg: string, pwd: string): string | undefined { if (!path.isAbsolute(arg)) { arg = path.join(pwd, arg); @@ -106,70 +93,111 @@ function getPath(arg: string, pwd: string): string | undefined { } function escapeWinPath(origPath: string) { - return origPath.replace(/\\/g,'\\\\'); + return origPath.replace(/\\/g, '\\\\'); } -export function activate(context: vscode.ExtensionContext) { - applyConfig(); - if (isWindows()) { - let server = net.createServer((socket) => { - socket.on('data', (data) => { - let [cmd, pwd, arg] = data.toString().trim().split('$$'); - cmd = cmd.trim(); pwd = pwd.trim(); arg = arg.trim(); - if (arg === "") { return } - if (cmd === 'open') { - let filename = getPath(arg, pwd); - if (!filename) { return } - vscode.window.showTextDocument(vscode.Uri.file(filename)); - } else if (cmd === 'add') { - let folder = getPath(arg, pwd); - if (!folder) { return } - vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, null, { - uri: vscode.Uri.file(folder) - }); - vscode.commands.executeCommand('workbench.view.explorer'); - } else if (cmd === 'rg') { - let [file, linestr, colstr] = arg.split(':'); - let filename = getPath(file, pwd); - if (!filename) { return }; - let line = parseInt(linestr)-1; - let col = parseInt(colstr)-1; - vscode.window.showTextDocument(vscode.Uri.file(filename)).then((ed) => { - ed.selection = new vscode.Selection(line, col, line, col); - }) - } - }) - +function processCommandInput(data: Buffer) { + let [cmd, pwd, arg] = data.toString().trim().split('$$'); + cmd = cmd.trim(); pwd = pwd.trim(); arg = arg.trim(); + if (arg === "") { return } + if (cmd === 'open') { + let filename = getPath(arg, pwd); + if (!filename) { return } + vscode.window.showTextDocument(vscode.Uri.file(filename)); + } else if (cmd === 'add') { + let folder = getPath(arg, pwd); + if (!folder) { return } + vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, null, { + uri: vscode.Uri.file(folder) }); - let idx = 0; - while (!fzfPipe) { - try { - let pipe = `\\\\?\\pipe\\fzf-pipe-${process.pid}`; - if (idx > 0) { pipe += `-${idx}`; } - server.listen(pipe); - fzfPipe = escapeWinPath(pipe); - } catch(e) { - if (e.code === 'EADDRINUSE') { - // Try again for a new address - ++idx; - } else { - // Bad news - throw e; - } + vscode.commands.executeCommand('workbench.view.explorer'); + } else if (cmd === 'rg') { + let [file, linestr, colstr] = arg.split(':'); + let filename = getPath(file, pwd); + if (!filename) { return }; + let line = parseInt(linestr) - 1; + let col = parseInt(colstr) - 1; + vscode.window.showTextDocument(vscode.Uri.file(filename)).then((ed) => { + ed.selection = new vscode.Selection(line, col, line, col); + }) + } +} + +function listenToFifo(fifo: string) { + fs.open(fifo, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK , (err, fd) => { + const pipe = new net.Socket({fd: fd, allowHalfOpen: true }); + pipe.on('data', (data) => { + processCommandInput(data); + }) + pipe.on('end', () => { + console.log('end!') + listenToFifo(fifo); + }) + }) +} + +function setupWindowsPipe() { + let server = net.createServer((socket) => { + socket.on('data', (data) => { + processCommandInput(data); + }) + }); + let idx = 0; + while (!fzfPipe) { + try { + let pipe = `\\\\?\\pipe\\fzf-pipe-${process.pid}`; + if (idx > 0) { pipe += `-${idx}`; } + server.listen(pipe); + fzfPipe = escapeWinPath(pipe); + } catch (e) { + if (e.code === 'EADDRINUSE') { + // Try again for a new address + ++idx; + } else { + // Bad news + throw e; } } - fzfPipeBatch = vscode.extensions.getExtension('rlivings39.fzf-quick-open')?.extensionPath ?? ""; - fzfPipeBatch = escapeWinPath(path.join(fzfPipeBatch, 'scripts', 'topipe.bat')); } - setupCodeCmd(); +} + +function setupPOSIXPipe() { + let idx = 0; + while (!fzfPipe && idx < 10) { + try { + let pipe = path.join(os.tmpdir(), `fzf-pipe-${process.pid}`); + if (idx > 0) { pipe += `-${idx}`; } + cp.execSync(`mkfifo -m 600 ${pipe}`); + fzfPipe = pipe; + } catch (e) { + // Try again for a new address + ++idx; + } + } + listenToFifo(fzfPipe); +} + +function setupPipesAndListeners() { + if (isWindows()) { + setupWindowsPipe(); + } else { + setupPOSIXPipe(); + } +} + +export function activate(context: vscode.ExtensionContext) { + applyConfig(); + setupPipesAndListeners(); + fzfPipeScript = vscode.extensions.getExtension('rlivings39.fzf-quick-open')?.extensionPath ?? ""; + fzfPipeScript = escapeWinPath(path.join(fzfPipeScript, 'scripts', 'topipe.' + (isWindows() ? "bat" : "sh"))); vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('fzf-quick-open')) { applyConfig(); } }) - let codeOpenFileCmd = `${fzfCmd} | ${fzfPipeBatch} open ${fzfPipe}`; - let codeOpenFolderCmd = `${fzfCmd} | ${fzfPipeBatch} add ${fzfPipe}`; + let codeOpenFileCmd = `${fzfCmd} | ${fzfPipeScript} open "${fzfPipe}"`; + let codeOpenFolderCmd = `${fzfCmd} | ${fzfPipeScript} add "${fzfPipe}"`; context.subscriptions.push(vscode.commands.registerCommand('fzf-quick-open.runFzfFile', () => { let term = showFzfTerminal(TERMINAL_NAME, fzfTerminal); @@ -199,7 +227,7 @@ export function activate(context: vscode.ExtensionContext) { return; } let term = showFzfTerminal(TERMINAL_NAME, fzfTerminal); - term.sendText(makeSearchCmd(pattern, codeCmd), true); + term.sendText(makeSearchCmd(pattern), true); })); context.subscriptions.push(vscode.commands.registerCommand('fzf-quick-open.runFzfSearchPwd', async () => { @@ -209,7 +237,7 @@ export function activate(context: vscode.ExtensionContext) { } let term = showFzfTerminal(TERMINAL_NAME_PWD, fzfTerminalPwd); moveToPwd(term); - term.sendText(makeSearchCmd(pattern, codeCmd), true); + term.sendText(makeSearchCmd(pattern), true); })); vscode.window.onDidCloseTerminal((terminal) => { @@ -249,8 +277,7 @@ async function getSearchText(): Promise { /** * Return the command used to invoke rg. Exported to allow unit testing. * @param pattern Pattern to search for - * @param codeCmd Command used to launch code */ -export function makeSearchCmd(pattern: string, codeCmd: string): string { - return `rg '${pattern}' ${rgCaseFlag} --vimgrep --color ansi | ${fzfCmd} --ansi | ${fzfPipeBatch} rg ${fzfPipe}`; +export function makeSearchCmd(pattern: string): string { + return `rg '${pattern}' ${rgCaseFlag} --vimgrep --color ansi | ${fzfCmd} --ansi | ${fzfPipeScript} rg "${fzfPipe}"`; } diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 815eed0..179768d 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -85,7 +85,7 @@ suite('fzf quick open uninit', async () => { async function verifyRgConfig(opt: fzfQuickOpen.rgoptions, flag: string) { let settings = vscode.workspace.getConfiguration('fzf-quick-open'); await settings.update('ripgrepSearchStyle', opt, vscode.ConfigurationTarget.Global); - let rgcmd = fzfQuickOpen.makeSearchCmd('pattern','code'); + let rgcmd = fzfQuickOpen.makeSearchCmd('pattern'); let expectedFlag = fzfQuickOpen.rgflagmap.get(opt); expect(rgcmd).contains(expectedFlag); expect(rgcmd).contains(flag);