Skip to content

Commit f40b6be

Browse files
authored
File Explorer: Adds SSH, Remote Window, and Machine Details Commands (#113)
Adds various commands to the node: ![image](https://github.com/tailscale-dev/vscode-tailscale/assets/40265/a6d4932f-778a-4034-a721-979c8d47c2fd) Signed-off-by: Tyler Smalley <tyler@tailscale.com>
1 parent 7f9bd0b commit f40b6be

4 files changed

Lines changed: 139 additions & 44 deletions

File tree

package.json

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -130,23 +130,43 @@
130130
],
131131
"view/item/context": [
132132
{
133-
"command": "tailscale.copyIPv4",
133+
"command": "tailscale.node.openRemoteCode",
134134
"when": "view == tailscale-node-explorer-view && viewItem == tailscale-peer-item",
135-
"group": "1_peer@1"
135+
"group": "1_action@1"
136136
},
137137
{
138-
"command": "tailscale.copyIPv6",
138+
"command": "tailscale.node.openTerminal",
139139
"when": "view == tailscale-node-explorer-view && viewItem == tailscale-peer-item",
140-
"group": "1_peer@1"
140+
"group": "1_action@2"
141141
},
142142
{
143-
"command": "tailscale.copyHostname",
143+
"command": "tailscale.node.copyIPv4",
144144
"when": "view == tailscale-node-explorer-view && viewItem == tailscale-peer-item",
145-
"group": "1_peer@1"
145+
"group": "2_copy@1"
146146
},
147147
{
148-
"command": "tailscale.ssh.delete",
149-
"group": "fileActions@2",
148+
"command": "tailscale.node.copyIPv6",
149+
"when": "view == tailscale-node-explorer-view && viewItem == tailscale-peer-item",
150+
"group": "2_copy@1"
151+
},
152+
{
153+
"command": "tailscale.node.copyHostname",
154+
"when": "view == tailscale-node-explorer-view && viewItem == tailscale-peer-item",
155+
"group": "2_copy@1"
156+
},
157+
{
158+
"command": "tailscale.node.openDetailsLink",
159+
"group": "3_control@1",
160+
"when": "view == tailscale-node-explorer-view && viewItem == tailscale-peer-item"
161+
},
162+
{
163+
"command": "tailscale.node.openRemoteCodeAtLocation",
164+
"group": "1_action@2",
165+
"when": "view == tailscale-node-explorer-view && viewItem == file-explorer-item"
166+
},
167+
{
168+
"command": "tailscale.node.fs.delete",
169+
"group": "2_fileAction@2",
150170
"when": "view == tailscale-node-explorer-view && viewItem == file-explorer-item"
151171
}
152172
]
@@ -188,20 +208,36 @@
188208
"category": "tsdev"
189209
},
190210
{
191-
"command": "tailscale.copyIPv4",
211+
"command": "tailscale.node.copyIPv4",
192212
"title": "Copy IPv4"
193213
},
194214
{
195-
"command": "tailscale.copyIPv6",
215+
"command": "tailscale.node.copyIPv6",
196216
"title": "Copy IPv6"
197217
},
198218
{
199-
"command": "tailscale.copyHostname",
219+
"command": "tailscale.node.copyHostname",
200220
"title": "Copy Hostname"
201221
},
202222
{
203-
"command": "tailscale.ssh.delete",
223+
"command": "tailscale.node.openTerminal",
224+
"title": "Open SSH"
225+
},
226+
{
227+
"command": "tailscale.node.openRemoteCode",
228+
"title": "Open Remote Connection"
229+
},
230+
{
231+
"command": "tailscale.node.openDetailsLink",
232+
"title": "Open Machine Details..."
233+
},
234+
{
235+
"command": "tailscale.node.fs.delete",
204236
"title": "Delete"
237+
},
238+
{
239+
"command": "tailscale.node.openRemoteCodeAtLocation",
240+
"title": "Open Remote Connection"
205241
}
206242
],
207243
"viewsContainers": {

src/extension.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -98,36 +98,6 @@ export async function activate(context: vscode.ExtensionContext) {
9898
})
9999
);
100100

101-
context.subscriptions.push(
102-
vscode.commands.registerCommand('tailscale.copyIPv4', async (node: PeerTree) => {
103-
const ip = node.TailscaleIPs[0];
104-
105-
if (!ip) {
106-
vscode.window.showErrorMessage(`No IPv4 address found for ${node.HostName}.`);
107-
return;
108-
}
109-
110-
await vscode.env.clipboard.writeText(ip);
111-
vscode.window.showInformationMessage(`Copied ${ip} to clipboard.`);
112-
})
113-
);
114-
115-
context.subscriptions.push(
116-
vscode.commands.registerCommand('tailscale.copyIPv6', async (node: PeerTree) => {
117-
const ip = node.TailscaleIPs[1];
118-
await vscode.env.clipboard.writeText(ip);
119-
vscode.window.showInformationMessage(`Copied ${ip} to clipboard.`);
120-
})
121-
);
122-
123-
context.subscriptions.push(
124-
vscode.commands.registerCommand('tailscale.copyHostname', async (node: PeerTree) => {
125-
const name = node.HostName;
126-
await vscode.env.clipboard.writeText(name);
127-
vscode.window.showInformationMessage(`Copied ${name} to clipboard.`);
128-
})
129-
);
130-
131101
vscode.window.registerWebviewViewProvider('tailscale-serve-view', servePanelProvider);
132102

133103
context.subscriptions.push(

src/node-explorer-provider.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export class NodeExplorerProvider
3131
this.fsProvider = new TSFileSystemProvider();
3232

3333
this.registerDeleteCommand();
34+
this.registerCopyIPv4Command();
35+
this.registerCopyIPv6Command();
36+
this.registerCopyHostnameCommand();
37+
this.registerOpenTerminalCommand();
38+
this.registerOpenRemoteCodeCommand();
39+
this.registerOpenRemoteCodeLocationCommand();
40+
this.registerOpenNodeDetailsCommand();
3441
}
3542

3643
dispose() {
@@ -151,7 +158,89 @@ export class NodeExplorerProvider
151158
}
152159

153160
registerDeleteCommand() {
154-
vscode.commands.registerCommand('tailscale.ssh.delete', this.delete.bind(this));
161+
vscode.commands.registerCommand('tailscale.node.fs.delete', this.delete.bind(this));
162+
}
163+
164+
registerOpenRemoteCodeLocationCommand() {
165+
vscode.commands.registerCommand(
166+
'tailscale.node.openRemoteCodeAtLocation',
167+
async (file: FileExplorer) => {
168+
const { hostname, resourcePath } = this.fsProvider.extractHostAndPath(file.uri);
169+
if (!hostname || !resourcePath) {
170+
return;
171+
}
172+
173+
// TODO: handle non-absolute paths
174+
this.openRemoteCodeLocationWindow(hostname, resourcePath, false);
175+
}
176+
);
177+
}
178+
179+
registerCopyIPv4Command() {
180+
vscode.commands.registerCommand('tailscale.node.copyIPv4', async (node: PeerTree) => {
181+
const ip = node.TailscaleIPs[0];
182+
183+
if (!ip) {
184+
vscode.window.showErrorMessage(`No IPv4 address found for ${node.HostName}.`);
185+
return;
186+
}
187+
188+
await vscode.env.clipboard.writeText(ip);
189+
vscode.window.showInformationMessage(`Copied ${ip} to clipboard.`);
190+
});
191+
}
192+
193+
registerCopyIPv6Command() {
194+
vscode.commands.registerCommand('tailscale.node.copyIPv6', async (node: PeerTree) => {
195+
const ip = node.TailscaleIPs[1];
196+
await vscode.env.clipboard.writeText(ip);
197+
vscode.window.showInformationMessage(`Copied ${ip} to clipboard.`);
198+
});
199+
}
200+
201+
registerCopyHostnameCommand() {
202+
vscode.commands.registerCommand('tailscale.node.copyHostname', async (node: PeerTree) => {
203+
const name = node.HostName;
204+
await vscode.env.clipboard.writeText(name);
205+
vscode.window.showInformationMessage(`Copied ${name} to clipboard.`);
206+
});
207+
}
208+
209+
registerOpenTerminalCommand() {
210+
vscode.commands.registerCommand('tailscale.node.openTerminal', async (node: PeerTree) => {
211+
const t = vscode.window.createTerminal(node.HostName);
212+
t.sendText(`ssh ${node.HostName}`);
213+
t.show();
214+
});
215+
}
216+
217+
registerOpenRemoteCodeCommand() {
218+
vscode.commands.registerCommand('tailscale.node.openRemoteCode', async (node: PeerTree) => {
219+
this.openRemoteCodeWindow(node.HostName, false);
220+
});
221+
}
222+
223+
registerOpenNodeDetailsCommand() {
224+
vscode.commands.registerCommand('tailscale.node.openDetailsLink', async (node: PeerTree) => {
225+
vscode.env.openExternal(
226+
vscode.Uri.parse(`https://login.tailscale.com/admin/machines/${node.TailscaleIPs[0]}`)
227+
);
228+
});
229+
}
230+
231+
openRemoteCodeWindow(host: string, reuseWindow: boolean) {
232+
vscode.commands.executeCommand('vscode.newWindow', {
233+
remoteAuthority: `ssh-remote+${host}`,
234+
reuseWindow,
235+
});
236+
}
237+
238+
openRemoteCodeLocationWindow(host: string, path: string, reuseWindow: boolean) {
239+
vscode.commands.executeCommand(
240+
'vscode.openFolder',
241+
vscode.Uri.from({ scheme: 'vscode-remote', authority: `ssh-remote+${host}`, path }),
242+
{ forceNewWindow: !reuseWindow }
243+
);
155244
}
156245

157246
async delete(file: FileExplorer) {

src/ts-file-system-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export class TSFileSystemProvider implements vscode.FileSystemProvider {
238238
});
239239
}
240240

241-
private extractHostAndPath(uri: vscode.Uri): { hostname: string | null; resourcePath: string } {
241+
public extractHostAndPath(uri: vscode.Uri): { hostname: string | null; resourcePath: string } {
242242
switch (uri.scheme) {
243243
case 'ts': {
244244
// removes leading slash

0 commit comments

Comments
 (0)