diff --git a/package.json b/package.json index 2f50a23..e79169f 100644 --- a/package.json +++ b/package.json @@ -83,8 +83,7 @@ "commandPalette": [ { "command": "sshfs.new", - "group": "SSH FS@1", - "when": "view == 'DISABLED'" + "group": "SSH FS@1" }, { "command": "sshfs.connect", @@ -100,8 +99,7 @@ }, { "command": "sshfs.configure", - "group": "SSH FS@5", - "when": "view == 'DISABLED'" + "group": "SSH FS@5" }, { "command": "sshfs.reload", @@ -115,7 +113,7 @@ "view/item/context": [ { "command": "sshfs.new", - "when": "view == 'sshfs-configs' && !viewItem && view == 'DISABLED'" + "when": "view == 'sshfs-configs' && !viewItem" }, { "command": "sshfs.connect", @@ -134,7 +132,7 @@ }, { "command": "sshfs.configure", - "when": "view == 'sshfs-configs' && viewItem && view == 'DISABLED'", + "when": "view == 'sshfs-configs' && viewItem", "group": "SSH FS@3" } ] diff --git a/src/extension.ts b/src/extension.ts index 90505ce..13f86a3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -53,10 +53,8 @@ export function activate(context: vscode.ExtensionContext) { if (name) func.call(manager, name); } - registerCommand('sshfs.new', async () => { - const name = await vscode.window.showInputBox({ placeHolder: 'Name for the new SSH file system', validateInput: invalidConfigName }); - if (name) vscode.window.showTextDocument(vscode.Uri.parse(`ssh:///${name}.sshfs.jsonc`), { preview: false }); - }); + registerCommand('sshfs.new', async () => manager.openSettings({ type: 'newconfig' })); + registerCommand('sshfs.settings', () => manager.openSettings()); registerCommand('sshfs.connect', (name?: string) => pickAndClick(manager.commandConnect, name, false)); registerCommand('sshfs.disconnect', (name?: string) => pickAndClick(manager.commandDisconnect, name, true)); @@ -65,7 +63,5 @@ export function activate(context: vscode.ExtensionContext) { registerCommand('sshfs.reload', loadConfigs); - registerCommand('sshfs.settings', () => manager.openSettings()); - vscode.window.createTreeView('sshfs-configs', { treeDataProvider: manager }); } diff --git a/src/manager.ts b/src/manager.ts index 09938bd..48a35ce 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -1,11 +1,12 @@ import { Client, ClientChannel } from 'ssh2'; import * as vscode from 'vscode'; -import { getConfig, getConfigs, loadConfigs, UPDATE_LISTENERS } from './config'; +import { getConfig, getConfigs, loadConfigs, loadConfigsRaw, UPDATE_LISTENERS } from './config'; import { FileSystemConfig } from './fileSystemConfig'; import * as Logging from './logging'; import SSHFileSystem from './sshFileSystem'; import { catchingPromise, toPromise } from './toPromise'; +import { Navigation } from './webviewMessages'; async function assertFs(man: Manager, uri: vscode.Uri) { const fs = await man.getFs(uri); @@ -269,16 +270,16 @@ export class Manager implements vscode.FileSystemProvider, vscode.TreeDataProvid } public async commandConfigure(name: string) { Logging.info(`Command received to configure ${name}`); - // openConfigurationEditor(name); - vscode.window.showWarningMessage('Use the SSH FS config editor to modify/delete configurations'); - // TODO: Make this work with the following code: - // this.openSettings(name); - // Would open the Settings UI, list all locations the (potentially) merged config originates from, - // and allow the user to pick (and edit) one of them. Maybe have a back-to-the-list button? + name = name.toLowerCase(); + let configs = await loadConfigsRaw(); + configs = configs.filter(c => c.name === name); + if (configs.length === 0) throw new Error('Unexpectedly found no matching configs?'); + const config = configs.length === 1 ? configs[0] : configs; + this.openSettings({ config, type: 'editconfig' }); } - public async openSettings() { - const { open } = await import('./settings'); - return open(this.context.extensionPath); + public async openSettings(navigation?: Navigation) { + const { open, navigate } = await import('./settings'); + return navigation ? navigate(navigation) : open(); } } diff --git a/src/settings.ts b/src/settings.ts index defd230..0f327d3 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -5,14 +5,20 @@ import * as vscode from 'vscode'; import { deleteConfig, loadConfigsRaw, updateConfig } from './config'; import { getLocations } from './fileSystemConfig'; import { toPromise } from './toPromise'; -import { Message } from './webviewMessages'; +import { Message, Navigation } from './webviewMessages'; let webviewPanel: vscode.WebviewPanel | undefined; +let pendingNavigation: Navigation | undefined; // Since the Extension Development Host runs with debugger, we can use this to detect if we're debugging const DEBUG: number | undefined = process.execArgv.find(a => a.includes('--inspect')) ? 3000 : undefined; if (DEBUG) console.warn('[vscode-sshfs] Detected we are running in debug mode'); +function getExtensionPath(): string | undefined { + const ext = vscode.extensions.getExtension('Kelvin.vscode-sshfs'); + return ext && ext.extensionPath; +} + async function getDebugContent(): Promise { if (!DEBUG) return false; const URL = `http://localhost:${DEBUG}/`; @@ -25,13 +31,15 @@ async function getDebugContent(): Promise { })); } -export async function open(extensionPath: string) { +export async function open() { if (!webviewPanel) { webviewPanel = vscode.window.createWebviewPanel('sshfs-settings', 'SSH-FS Settings', vscode.ViewColumn.One, { enableFindWidget: true, enableScripts: true }); webviewPanel.onDidDispose(() => webviewPanel = undefined); webviewPanel.webview.onDidReceiveMessage(handleMessage); let content = await getDebugContent().catch((e: Error) => (vscode.window.showErrorMessage(e.message), null)); if (!content) { + const extensionPath = getExtensionPath(); + if (!extensionPath) throw new Error('Could not get extensionPath'); // If we got here, we're either not in debug mode, or something went wrong (and an error message is shown) content = fs.readFileSync(path.resolve(extensionPath, 'webview/build/index.html')).toString(); // Built index.html has e.g. `href="/static/js/stuff.js"`, need to make it use vscode-resource: and point to the built static directory @@ -42,13 +50,27 @@ export async function open(extensionPath: string) { webviewPanel.reveal(); } -function postMessage(message: Message) { +export async function navigate(navigation: Navigation) { + pendingNavigation = navigation; + postMessage({ navigation, type: 'navigate' }); + return open(); +} + +function postMessage(message: T) { if (!webviewPanel) return; webviewPanel.webview.postMessage(message); } async function handleMessage(message: Message): Promise { console.log('Got message:', message); + if (message.type === 'navigated') pendingNavigation = undefined; + if (pendingNavigation) { + postMessage({ + type: 'navigate', + navigation: pendingNavigation, + }); + pendingNavigation = undefined; + } switch (message.type) { case 'requestData': { const configs = await loadConfigsRaw(); diff --git a/src/webviewMessages.ts b/src/webviewMessages.ts index 8f2ae68..3cf4848 100644 --- a/src/webviewMessages.ts +++ b/src/webviewMessages.ts @@ -1,5 +1,7 @@ import { ConfigLocation, FileSystemConfig } from './fileSystemConfig'; +/* Type of messages*/ + export interface RequestDataMessage { type: 'requestData'; } @@ -34,6 +36,15 @@ export interface PromptPathResultMessage { uniqueId?: string; } +export interface NavigateMessage { + type: 'navigate'; + navigation: Navigation; +} +export interface NavigatedMessage { + type: 'navigated'; + navigation: Navigation; +} + export interface MessageTypes { requestData: RequestDataMessage; responseData: ResponseDataMessage; @@ -41,6 +52,19 @@ export interface MessageTypes { saveConfigResult: SaveConfigResultMessage; promptPath: PromptPathMessage; promptPathResult: PromptPathResultMessage; + navigate: NavigateMessage; + navigated: NavigatedMessage; } export type Message = MessageTypes[keyof MessageTypes]; + +/* Types related to NavigateMessage */ + +export interface NewConfigNavigation { + type: 'newconfig'; +} +export interface EditConfigNavigation { + type: 'editconfig'; + config: FileSystemConfig | FileSystemConfig[]; +} +export type Navigation = NewConfigNavigation | EditConfigNavigation; diff --git a/webview/src/ConfigList/index.tsx b/webview/src/ConfigList/index.tsx index 5a8f410..3bc5032 100644 --- a/webview/src/ConfigList/index.tsx +++ b/webview/src/ConfigList/index.tsx @@ -12,6 +12,7 @@ interface DispatchProps { } interface OwnProps { configs?: FileSystemConfig[]; + displayName?(config: FileSystemConfig): string | undefined; } class ConfigList extends React.Component { public render() { @@ -24,8 +25,10 @@ class ConfigList extends React.Component ; } public editConfigClickHandler(config: FileSystemConfig) { + const { displayName } = this.props; + const name = displayName && displayName(config) || config.label || config.name; const onClick = () => this.props.editConfig(config); - return
  • {config.label || config.name}
  • ; + return
  • {name}
  • ; } } diff --git a/webview/src/ConfigLocator/index.css b/webview/src/ConfigLocator/index.css new file mode 100644 index 0000000..b3553f3 --- /dev/null +++ b/webview/src/ConfigLocator/index.css @@ -0,0 +1,4 @@ + +div.ConfigLocator { + padding: 5px; +} diff --git a/webview/src/ConfigLocator/index.tsx b/webview/src/ConfigLocator/index.tsx new file mode 100644 index 0000000..6ee78ac --- /dev/null +++ b/webview/src/ConfigLocator/index.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import ConfigList from 'src/ConfigList'; +import { connect, pickProperties } from 'src/redux'; +import { FileSystemConfig, formatConfigLocation } from 'src/types/fileSystemConfig'; +import { IConfigLocatorState } from 'src/view'; +import './index.css'; + +function displayName(config: FileSystemConfig) { + return formatConfigLocation(config._location); +} + +interface StateProps { + configs: FileSystemConfig[]; + name: string; +} +class ConfigLocator extends React.Component { + public render() { + const { configs, name } = this.props; + return
    +

    Locations of {name}

    + +
    ; + } +} + +interface SubState { view: IConfigLocatorState } +export default connect(ConfigLocator)( + state => pickProperties(state.view, 'configs', 'name'), +); diff --git a/webview/src/Homescreen/index.tsx b/webview/src/Homescreen/index.tsx index c45b5a0..c1b5a9d 100644 --- a/webview/src/Homescreen/index.tsx +++ b/webview/src/Homescreen/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import ConfigList from 'src/ConfigList'; import { receivedData } from 'src/data/actions'; -import { connect } from 'src/redux'; +import { connect, pickProperties } from 'src/redux'; import { ConfigLocation, FileSystemConfig, formatConfigLocation, groupByLocation } from 'src/types/fileSystemConfig'; import { openNewConfig } from 'src/view/actions'; import { API } from 'src/vscode'; @@ -9,11 +9,10 @@ import './index.css'; interface StateProps { configs: FileSystemConfig[]; - locations: ConfigLocation[]; } interface DispatchProps { refresh(): void; - add(locations: ConfigLocation[]): void; + add(): void; } class Homescreen extends React.Component { public componentDidMount() { @@ -25,7 +24,7 @@ class Homescreen extends React.Component { return

    Configurations

    - + {grouped.map(([loc, configs]) => this.createGroup(loc, configs))}
    ; } @@ -35,13 +34,12 @@ class Homescreen extends React.Component { ; } - public add = () => this.props.add(this.props.locations); } export default connect(Homescreen)( - state => ({ configs: state.data.configs, locations: state.data.locations }), + state => pickProperties(state.data, 'configs'), dispatch => ({ - add: locations => dispatch(openNewConfig(locations)), + add: () => dispatch(openNewConfig()), refresh: () => (dispatch(receivedData([], [])), API.postMessage({ type: 'requestData' })), }), ); diff --git a/webview/src/NewConfig/index.tsx b/webview/src/NewConfig/index.tsx index 03d808c..0b3d92d 100644 --- a/webview/src/NewConfig/index.tsx +++ b/webview/src/NewConfig/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { FieldDropdown } from 'src/FieldTypes/dropdown'; import { FieldGroup } from 'src/FieldTypes/group'; import { FieldString } from 'src/FieldTypes/string'; -import { connect, pickProperties } from 'src/redux'; +import { connect, pickProperties, State } from 'src/redux'; import { ConfigLocation, formatConfigLocation, invalidConfigName } from 'src/types/fileSystemConfig'; import { INewConfigState } from 'src/view'; import { newConfigSetLocation, newConfigSetName, openConfigEditor, openStartScreen } from 'src/view/actions'; @@ -53,9 +53,13 @@ class NewConfig extends React.Component { public confirm = () => this.props.confirm(this.props.name, this.props.location); } -interface SubState { view: INewConfigState } +interface SubState extends State { view: INewConfigState } export default connect(NewConfig)( - (state) => pickProperties(state.view, 'location', 'locations', 'name'), + (state) => ({ + ...pickProperties(state.view, 'name'), + ...pickProperties(state.data, 'locations'), + location: state.view.location || state.data.locations[0], + }), (dispatch) => ({ cancel: () => dispatch(openStartScreen()), setLocation: loc => dispatch(newConfigSetLocation(loc)), diff --git a/webview/src/router.tsx b/webview/src/router.tsx index b1a60a3..c17669a 100644 --- a/webview/src/router.tsx +++ b/webview/src/router.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import ConfigEditor from './ConfigEditor'; +import ConfigLocator from './ConfigLocator'; import Homescreen from './Homescreen'; import NewConfig from './NewConfig'; import { connect, State } from './redux'; @@ -11,6 +12,8 @@ function Router(props: StateProps) { switch (props.view) { case 'configeditor': return ; + case 'configlocator': + return ; case 'newconfig': return ; case 'startscreen': diff --git a/webview/src/types/webviewMessages.ts b/webview/src/types/webviewMessages.ts index 8f2ae68..3cf4848 100644 --- a/webview/src/types/webviewMessages.ts +++ b/webview/src/types/webviewMessages.ts @@ -1,5 +1,7 @@ import { ConfigLocation, FileSystemConfig } from './fileSystemConfig'; +/* Type of messages*/ + export interface RequestDataMessage { type: 'requestData'; } @@ -34,6 +36,15 @@ export interface PromptPathResultMessage { uniqueId?: string; } +export interface NavigateMessage { + type: 'navigate'; + navigation: Navigation; +} +export interface NavigatedMessage { + type: 'navigated'; + navigation: Navigation; +} + export interface MessageTypes { requestData: RequestDataMessage; responseData: ResponseDataMessage; @@ -41,6 +52,19 @@ export interface MessageTypes { saveConfigResult: SaveConfigResultMessage; promptPath: PromptPathMessage; promptPathResult: PromptPathResultMessage; + navigate: NavigateMessage; + navigated: NavigatedMessage; } export type Message = MessageTypes[keyof MessageTypes]; + +/* Types related to NavigateMessage */ + +export interface NewConfigNavigation { + type: 'newconfig'; +} +export interface EditConfigNavigation { + type: 'editconfig'; + config: FileSystemConfig | FileSystemConfig[]; +} +export type Navigation = NewConfigNavigation | EditConfigNavigation; diff --git a/webview/src/view/actions.ts b/webview/src/view/actions.ts index c63a8ff..ac2caab 100644 --- a/webview/src/view/actions.ts +++ b/webview/src/view/actions.ts @@ -9,6 +9,7 @@ export enum ActionType { NEWCONFIG_SETLOCATION = 'NEWCONFIG_SETLOCATION', // ConfigEditor OPEN_CONFIGEDITOR = 'OPEN_CONFIGEDITOR', + OPEN_CONFIGLOCATOR = 'OPEN_CONFIGLOCATOR', CONFIGEDITOR_SETNEWCONFIG = 'CONFIGEDITOR_SETNEWCONFIG', CONFIGEDITOR_SETSTATUSMESSAGE = 'CONFIGEDITOR_SETSTATUSMESSAGE', } @@ -22,6 +23,7 @@ export interface ActionTypes { NEWCONFIG_SETLOCATION: IActionNewConfigSetLocation; // ConfigEditor OPEN_CONFIGEDITOR: IActionOpenConfigEditor; + OPEN_CONFIGLOCATOR: IActionOpenConfigLocator; CONFIGEDITOR_SETNEWCONFIG: IActionConfigEditorSetNewConfig; CONFIGEDITOR_SETSTATUSMESSAGE: IActionConfigEditorSetStatusMessage; } @@ -44,11 +46,10 @@ export function openStartScreen(): IActionOpenStartscreen { export interface IActionOpenNewConfig extends IAction { type: ActionType.OPEN_NEWCONFIG; - locations: ConfigLocation[]; name: string; } -export function openNewConfig(locations: ConfigLocation[], name = 'unnamed'): IActionOpenNewConfig { - return { type: ActionType.OPEN_NEWCONFIG, locations, name }; +export function openNewConfig(name = 'unnamed'): IActionOpenNewConfig { + return { type: ActionType.OPEN_NEWCONFIG, name }; } export interface IActionNewConfigSetLocation extends IAction { @@ -77,6 +78,15 @@ export function openConfigEditor(config: FileSystemConfig): IActionOpenConfigEdi return { type: ActionType.OPEN_CONFIGEDITOR, config }; } +export interface IActionOpenConfigLocator extends IAction { + type: ActionType.OPEN_CONFIGLOCATOR; + configs: FileSystemConfig[]; + name: string; +} +export function openConfigLocator(configs: FileSystemConfig[], name: string): IActionOpenConfigLocator { + return { type: ActionType.OPEN_CONFIGLOCATOR, configs, name }; +} + export interface IActionConfigEditorSetNewConfig extends IAction { type: ActionType.CONFIGEDITOR_SETNEWCONFIG; config: FileSystemConfig; diff --git a/webview/src/view/index.ts b/webview/src/view/index.ts index 2a72bb2..6d5b495 100644 --- a/webview/src/view/index.ts +++ b/webview/src/view/index.ts @@ -1,5 +1,6 @@ import { Store } from 'redux'; +import { addListener, API } from 'src/vscode'; import * as actions from './actions'; export { reducer } from './reducers'; @@ -7,5 +8,22 @@ export { actions } export * from './state'; export function initStore(store: Store) { - // Nothing really + addListener((msg) => { + const { navigation } = msg; + switch (navigation.type) { + case 'newconfig': + return store.dispatch(actions.openNewConfig()); + case 'editconfig': { + let { config } = navigation; + if (Array.isArray(config)) { + if (config.length !== 1) { + return store.dispatch(actions.openConfigLocator(config, config[0].name)); + } + config = config[0]; + } + return store.dispatch(actions.openConfigEditor(config)); + } + } + API.postMessage({ type: 'navigated', navigation }); + }, 'navigate'); } diff --git a/webview/src/view/reducers.ts b/webview/src/view/reducers.ts index b582a0f..58e4b13 100644 --- a/webview/src/view/reducers.ts +++ b/webview/src/view/reducers.ts @@ -8,8 +8,8 @@ export function reducer(state = DEFAULT_STATE, action: Action): IState { return { ...state, view: 'startscreen' }; // New Config case ActionType.OPEN_NEWCONFIG: { - const { locations, name } = action; - return { ...state, view: 'newconfig', name, locations, location: locations[0] }; + const { name } = action; + return { ...state, view: 'newconfig', name, location: undefined }; } case ActionType.NEWCONFIG_SETNAME: return { ...state as INewConfigState, name: action.name }; @@ -20,6 +20,10 @@ export function reducer(state = DEFAULT_STATE, action: Action): IState { const { config } = action; return { ...state, view: 'configeditor', oldConfig: config, newConfig: config }; } + case ActionType.OPEN_CONFIGLOCATOR: { + const { name, configs } = action; + return { ...state, view: 'configlocator', name, configs }; + } case ActionType.CONFIGEDITOR_SETNEWCONFIG: return { ...state as IConfigEditorState, newConfig: action.config }; case ActionType.CONFIGEDITOR_SETSTATUSMESSAGE: diff --git a/webview/src/view/state.ts b/webview/src/view/state.ts index 8bfe3e7..6ad9fb4 100644 --- a/webview/src/view/state.ts +++ b/webview/src/view/state.ts @@ -7,8 +7,7 @@ interface IViewState { export type IStartScreenState = IViewState<'startscreen'>; export interface INewConfigState extends IViewState<'newconfig'> { - locations: ConfigLocation[]; - location: ConfigLocation; + location?: ConfigLocation; name: string; } @@ -18,7 +17,12 @@ export interface IConfigEditorState extends IViewState<'configeditor'> { statusMessage?: string; } -export type IState = IStartScreenState | INewConfigState | IConfigEditorState; +export interface IConfigLocatorState extends IViewState<'configlocator'> { + configs: FileSystemConfig[]; + name: string; +} + +export type IState = IStartScreenState | INewConfigState | IConfigEditorState | IConfigLocatorState; export const DEFAULT_STATE: IState = { view: 'startscreen',