mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(remote-dev): Remote Development in the project popup, opened in a new or this window
- The title bar project popup lists Remote Development… next to Open… and Clone Repository…. - Opening a remote project follows "Open project in" (new window / this window / ask) when this window already has a project, and confirms leaving the current one first. - A remote project already open in another window focuses that window. - A new window connects to the host itself and shows the connection progress on its welcome screen. - Recent remote projects get Open in New Window.
This commit is contained in:
@@ -296,6 +296,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
sdk::sdk_python_packages,
|
||||
sdk::sdk_python_pip,
|
||||
window::window_open_project,
|
||||
window::window_open_remote,
|
||||
window::window_new,
|
||||
window::window_open_settings,
|
||||
window::window_cycle,
|
||||
|
||||
@@ -17,6 +17,19 @@ pub async fn window_open_project(app: AppHandle, path: PathBuf, new_window: bool
|
||||
windows::open_project_window(&app, &path, new_window)
|
||||
}
|
||||
|
||||
/// [`window_open_project`] for folder `path` on the host of SSH configuration
|
||||
/// `ssh_config` (Remote Development); a new window connects itself.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn window_open_remote(
|
||||
app: AppHandle,
|
||||
ssh_config: String,
|
||||
path: String,
|
||||
new_window: bool,
|
||||
) -> IdeResult<OpenOutcome> {
|
||||
windows::open_remote_window(&app, &ssh_config, &path, new_window)
|
||||
}
|
||||
|
||||
/// Opens an empty (welcome) window; returns its label.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
|
||||
@@ -47,6 +47,16 @@ impl Connections {
|
||||
self.0.lock().get(window).cloned()
|
||||
}
|
||||
|
||||
/// The window connected to folder `root` on the host of SSH configuration `ssh_config`.
|
||||
pub fn window_of(&self, ssh_config: &str, root: &str) -> Option<String> {
|
||||
let root = root.trim_end_matches('/');
|
||||
self.0
|
||||
.lock()
|
||||
.iter()
|
||||
.find(|(_, c)| c.ssh_config == ssh_config && c.root.trim_end_matches('/') == root)
|
||||
.map(|(label, _)| label.clone())
|
||||
}
|
||||
|
||||
pub fn disconnect(&self, window: &str) {
|
||||
if let Some(c) = self.0.lock().remove(window) {
|
||||
c.peer.kill();
|
||||
|
||||
@@ -38,12 +38,22 @@ const SETTINGS_MIN_SIZE: (f64, f64) = (640.0, 420.0);
|
||||
pub struct Launch {
|
||||
/// Project folder to open.
|
||||
pub project: Option<ide_core::IpcPath>,
|
||||
/// Remote project to open (Remote Development).
|
||||
pub remote: Option<RemoteLaunch>,
|
||||
/// Settings windows: the page to show.
|
||||
pub settings_page: Option<String>,
|
||||
/// Settings windows: edit the global layer only ("Settings for New Projects").
|
||||
pub new_projects: bool,
|
||||
}
|
||||
|
||||
/// A project folder on the host of an SSH configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoteLaunch {
|
||||
pub ssh_config: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WindowsState {
|
||||
launches: Mutex<HashMap<String, Launch>>,
|
||||
@@ -184,7 +194,7 @@ pub fn find_project_window(app: &AppHandle, root: &Path) -> Option<WebviewWindow
|
||||
}
|
||||
|
||||
fn has_project(app: &AppHandle, label: &str) -> bool {
|
||||
app.try_state::<AppState>().is_some_and(|s| s.project(label).is_ok())
|
||||
app.try_state::<AppState>().is_some_and(|s| s.project(label).is_ok() || s.remote_dev.get(label).is_some())
|
||||
}
|
||||
|
||||
/// The focused window, else the last focused, else any.
|
||||
@@ -272,12 +282,17 @@ pub fn create_main_window(app: &AppHandle) -> IdeResult<WebviewWindow> {
|
||||
|
||||
/// Creates a new project window; `project` is opened by its frontend on load.
|
||||
pub fn create_window(app: &AppHandle, project: Option<PathBuf>) -> IdeResult<WebviewWindow> {
|
||||
let title = project.as_deref().and_then(Path::file_name).map(|n| n.to_string_lossy().into_owned());
|
||||
create_window_for(app, title, Launch { project: project.map(Into::into), ..Default::default() })
|
||||
}
|
||||
|
||||
fn create_window_for(app: &AppHandle, title: Option<String>, launch: Launch) -> IdeResult<WebviewWindow> {
|
||||
let windows = app.webview_windows();
|
||||
let label = next_label(windows.keys().map(String::as_str));
|
||||
let mut config = window_config(app)?;
|
||||
config.label = label.clone();
|
||||
if let Some(name) = project.as_deref().and_then(Path::file_name) {
|
||||
config.title = name.to_string_lossy().into_owned();
|
||||
if let Some(title) = title {
|
||||
config.title = title;
|
||||
}
|
||||
// Cascade from the window the user is looking at; window-state overrides
|
||||
// this with the remembered geometry when it has one for the label.
|
||||
@@ -289,7 +304,7 @@ pub fn create_window(app: &AppHandle, project: Option<PathBuf>) -> IdeResult<Web
|
||||
config.y = Some(pos.y + CASCADE);
|
||||
config.center = false;
|
||||
}
|
||||
app.state::<WindowsState>().set_launch(&label, Launch { project: project.map(Into::into), ..Default::default() });
|
||||
app.state::<WindowsState>().set_launch(&label, launch);
|
||||
let window = build_project_window(app, config)?;
|
||||
focus(&window);
|
||||
Ok(window)
|
||||
@@ -332,6 +347,25 @@ pub fn open_project_window(app: &AppHandle, path: &Path, new_window: bool) -> Id
|
||||
Ok(OpenOutcome::Opened { label: window.label().to_owned() })
|
||||
}
|
||||
|
||||
/// [`open_project_window`] for folder `path` on the host of SSH configuration
|
||||
/// `ssh_config` (Remote Development).
|
||||
pub fn open_remote_window(app: &AppHandle, ssh_config: &str, path: &str, new_window: bool) -> IdeResult<OpenOutcome> {
|
||||
tracing::info!("open_remote_window {ssh_config}:{path} new_window={new_window}");
|
||||
let windows = app.webview_windows();
|
||||
let open = app.try_state::<AppState>().and_then(|s| s.remote_dev.window_of(ssh_config, path));
|
||||
if let Some(w) = open.and_then(|label| windows.get(&label)) {
|
||||
focus(w);
|
||||
return Ok(OpenOutcome::Focused { label: w.label().to_owned() });
|
||||
}
|
||||
if !new_window {
|
||||
return Ok(OpenOutcome::Current);
|
||||
}
|
||||
let title = path.trim_end_matches('/').rsplit('/').next().filter(|n| !n.is_empty() && *n != "~").map(str::to_owned);
|
||||
let remote = RemoteLaunch { ssh_config: ssh_config.to_owned(), path: path.to_owned() };
|
||||
let window = create_window_for(app, title, Launch { remote: Some(remote), ..Default::default() })?;
|
||||
Ok(OpenOutcome::Opened { label: window.label().to_owned() })
|
||||
}
|
||||
|
||||
/// Opens `path` from the command line / a second instance: focuses the window
|
||||
/// that has it, else reuses a window without a project, else makes a new one.
|
||||
pub fn open_path(app: &AppHandle, path: &Path) {
|
||||
@@ -402,7 +436,7 @@ pub fn open_settings_window(
|
||||
let windows = app.state::<WindowsState>();
|
||||
if let Some(existing) = app.get_webview_window(&label) {
|
||||
if page.is_some() || new_projects {
|
||||
windows.set_launch(&label, Launch { project: None, settings_page: page, new_projects });
|
||||
windows.set_launch(&label, Launch { settings_page: page, new_projects, ..Default::default() });
|
||||
let _ = app.emit_to(label.as_str(), LAUNCH_EVENT, ());
|
||||
}
|
||||
focus(&existing);
|
||||
@@ -425,7 +459,7 @@ pub fn open_settings_window(
|
||||
if let Some(color) = background.as_deref().and_then(hex_color) {
|
||||
config.background_color = Some(color);
|
||||
}
|
||||
windows.set_launch(&label, Launch { project: None, settings_page: page, new_projects });
|
||||
windows.set_launch(&label, Launch { settings_page: page, new_projects, ..Default::default() });
|
||||
let window = WebviewWindowBuilder::from_config(app, &config)
|
||||
.and_then(|b| b.parent(&parent))
|
||||
.and_then(|b| b.build())
|
||||
|
||||
@@ -578,6 +578,11 @@ export const commands = {
|
||||
* itself (`current`). A project already open in some window is focused instead.
|
||||
*/
|
||||
windowOpenProject: (path: string, newWindow: boolean) => typedError<OpenOutcome, IdeError>(__TAURI_INVOKE("window_open_project", { path, newWindow })),
|
||||
/**
|
||||
* [`window_open_project`] for folder `path` on the host of SSH configuration
|
||||
* `ssh_config` (Remote Development); a new window connects itself.
|
||||
*/
|
||||
windowOpenRemote: (sshConfig: string, path: string, newWindow: boolean) => typedError<OpenOutcome, IdeError>(__TAURI_INVOKE("window_open_remote", { sshConfig, path, newWindow })),
|
||||
/** Opens an empty (welcome) window; returns its label. */
|
||||
windowNew: () => typedError<string, IdeError>(__TAURI_INVOKE("window_new")),
|
||||
/**
|
||||
@@ -593,6 +598,8 @@ export const commands = {
|
||||
windowTakeLaunch: () => __TAURI_INVOKE<{
|
||||
/** Project folder to open. */
|
||||
project: IpcPath | null,
|
||||
/** Remote project to open (Remote Development). */
|
||||
remote: RemoteLaunch | null,
|
||||
/** Settings windows: the page to show. */
|
||||
settingsPage: string | null,
|
||||
/** Settings windows: edit the global layer only ("Settings for New Projects"). */
|
||||
@@ -3510,6 +3517,8 @@ export type LanguagesSettings = {
|
||||
export type Launch = {
|
||||
/** Project folder to open. */
|
||||
project: IpcPath | null,
|
||||
/** Remote project to open (Remote Development). */
|
||||
remote: RemoteLaunch | null,
|
||||
/** Settings windows: the page to show. */
|
||||
settingsPage: string | null,
|
||||
/** Settings windows: edit the global layer only ("Settings for New Projects"). */
|
||||
@@ -4461,6 +4470,12 @@ export type RemoteEntry = {
|
||||
permissions: string,
|
||||
};
|
||||
|
||||
/** A project folder on the host of an SSH configuration. */
|
||||
export type RemoteLaunch = {
|
||||
sshConfig: string,
|
||||
path: string,
|
||||
};
|
||||
|
||||
export type RemoteOp = { op: "fetch" } | { op: "pull"; rebase: boolean } | { op: "push"; force_with_lease: boolean; set_upstream: boolean;
|
||||
/** Push dialog › Push tags. */
|
||||
tags?: PushTags | null } |
|
||||
|
||||
+15
-31
@@ -7,9 +7,10 @@ import type { Action } from "@/platform/actions";
|
||||
import { t } from "@/platform/i18n";
|
||||
import { call, commands, isTauri } from "@/platform/ipc";
|
||||
import { basename, confirmLeaveProject, openProject, provideStartupLaunch, useProject } from "@/platform/project";
|
||||
import { connectRemote } from "@/platform/remote";
|
||||
import { holdSplash } from "@/platform/splash";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { ask } from "@/workbench/messages";
|
||||
import { chooseProjectWindow } from "@/workbench/projectWindow";
|
||||
|
||||
export const LAUNCH_EVENT = "openidea://launch";
|
||||
|
||||
@@ -58,42 +59,14 @@ export async function openProjectInWindow(path: string, newWindow: boolean): Pro
|
||||
}
|
||||
}
|
||||
|
||||
/** System Settings › Project › "Open project in": `new` | `same` | `ask`. */
|
||||
export function openPolicy(): "ask" | "new" | "same" {
|
||||
const v = useProject.getState().settings?.general?.openProjectIn;
|
||||
return v === "new" || v === "same" ? v : "ask";
|
||||
}
|
||||
|
||||
/** Opens `path` following "Open project in" when this window already has a project. */
|
||||
export async function openProjectWithPolicy(path: string): Promise<void> {
|
||||
if (desktopOnly()) return;
|
||||
const current = useProject.getState().project;
|
||||
if (!current) return openProjectInWindow(path, false);
|
||||
if (current.root === path) return;
|
||||
let mode = openPolicy();
|
||||
if (mode === "ask") {
|
||||
const r = await ask({
|
||||
title: t("app.window.askTitle"),
|
||||
body: t("app.window.askBody", { name: basename(path) }),
|
||||
buttons: [
|
||||
{ id: "new", label: t("app.window.askNew") },
|
||||
{ id: "same", label: t("app.window.askThis"), primary: true },
|
||||
{ id: "cancel", label: t("common.cancel") },
|
||||
],
|
||||
checkbox: t("app.window.askRemember"),
|
||||
});
|
||||
if (r.button !== "same" && r.button !== "new") return;
|
||||
mode = r.button;
|
||||
if (r.checked) {
|
||||
try {
|
||||
useProject.setState({
|
||||
settings: await call(commands.settingsSet("global", "general.openProjectIn", JSON.stringify(mode))),
|
||||
});
|
||||
} catch {
|
||||
/* the choice still applies to this open */
|
||||
}
|
||||
}
|
||||
}
|
||||
const mode = await chooseProjectWindow(basename(path));
|
||||
if (!mode) return;
|
||||
if (mode === "new") return openProjectInWindow(path, true);
|
||||
try {
|
||||
// A window that already has the project is focused instead.
|
||||
@@ -121,6 +94,17 @@ export async function consumeLaunch(): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
const launch = await commands.windowTakeLaunch().catch(() => null);
|
||||
if (launch?.project) await openHere(launch.project);
|
||||
else if (launch?.remote) {
|
||||
const { sshConfig, path } = launch.remote;
|
||||
await connectRemote(sshConfig, path).catch((e) =>
|
||||
notifications().notify({
|
||||
level: "error",
|
||||
title: t("app.window.openFailed", { name: basename(path) }),
|
||||
body: errorText(e),
|
||||
key: `app.open:${path}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function windowActions(): Action[] {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/** Picks an SSH configuration and a folder on that host, then opens it in this window. */
|
||||
/** Picks an SSH configuration and a folder on that host, then opens it in this or a new window. */
|
||||
import { useState } from "react";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { useTx } from "@/platform/i18n";
|
||||
import { call, commands, type RemoteEntry } from "@/platform/ipc";
|
||||
import { connectRemote, useRemoteProgress } from "@/platform/remote";
|
||||
import { useRemoteProgress } from "@/platform/remote";
|
||||
import { sshLabel } from "@/platform/remoteSettings";
|
||||
import { useWorkflowSettings } from "@/platform/workflowSettings";
|
||||
import { Button, Dialog, Icon, Input, ProgressBar, Select, Spinner } from "@/ui";
|
||||
import { openRemoteProject } from "@/workbench/projectWindow";
|
||||
import {
|
||||
browsableFolders,
|
||||
closeConnectDialog,
|
||||
@@ -52,8 +53,9 @@ function ConnectDialog() {
|
||||
setError(null);
|
||||
setBusy(true);
|
||||
try {
|
||||
await connectRemote(ssh, path.trim() || "~");
|
||||
closeConnectDialog();
|
||||
const target = path.trim() || "~";
|
||||
if (await openRemoteProject(ssh, target)) closeConnectDialog();
|
||||
else setBusy(false);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
setBusy(false);
|
||||
@@ -62,6 +64,7 @@ function ConnectDialog() {
|
||||
|
||||
const options = configs.map((c) => ({ value: c.id, label: c.name.trim() || sshLabel(c), detail: sshLabel(c) }));
|
||||
const ready = !!ssh && !busy && !!path.trim();
|
||||
const connecting = useRemoteProgress((s) => s.progress !== null);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -70,7 +73,7 @@ function ConnectDialog() {
|
||||
onClose={closeConnectDialog}
|
||||
width={560}
|
||||
buttons={[
|
||||
{ label: busy ? "Connecting…" : "Open", role: "default", disabled: !ready, onClick: () => void connect() },
|
||||
{ label: connecting ? "Connecting…" : "Open", role: "default", disabled: !ready, onClick: () => void connect() },
|
||||
{ label: "Cancel", role: "cancel" },
|
||||
]}
|
||||
>
|
||||
@@ -83,7 +86,7 @@ function ConnectDialog() {
|
||||
>
|
||||
<p className={styles.hint}>
|
||||
{tx(
|
||||
"The project runs on the remote host; this window shows it. The IDE backend is installed there on first use.",
|
||||
"The project runs on the remote host; the IDE backend is installed there on first use.",
|
||||
)}
|
||||
</p>
|
||||
<label className={styles.label}>{tx("SSH configuration:")}</label>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function activate(): () => void {
|
||||
{
|
||||
id: "remote.connect",
|
||||
title: "Remote Development…",
|
||||
description: "Opens a project folder on an SSH host in this window; the IDE backend runs on that host.",
|
||||
description: "Opens a project folder on an SSH host; the IDE backend runs on that host.",
|
||||
category: "File",
|
||||
icon: "folder",
|
||||
keywords: ["ssh", "remote", "server", "gateway", "connect", "host"],
|
||||
|
||||
@@ -14,6 +14,7 @@ type Entry =
|
||||
const ACTIONS: { id: string; label: string; icon: IconName }[] = [
|
||||
{ id: "file.openProject", label: "Open…", icon: "folder" },
|
||||
{ id: "vcs.clone", label: "Clone Repository…", icon: "branch" },
|
||||
{ id: "remote.connect", label: "Remote Development…", icon: "terminal" },
|
||||
];
|
||||
|
||||
function useOpenProjects(): OpenProject[] {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState, type KeyboardEvent, type MouseEvent, type ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
import type { RecentProject } from "@/platform/ipc";
|
||||
import { call, commands, type RecentProject } from "@/platform/ipc";
|
||||
import { parseRemoteLocation } from "@/platform/remote";
|
||||
import {
|
||||
HighlightedText,
|
||||
@@ -39,8 +39,8 @@ export function ProjectBadge({ name, className }: { name: string; className?: st
|
||||
}
|
||||
|
||||
export function recentMenu(project: RecentProject, onOpened?: () => void): MenuItem[] {
|
||||
// A remote project opens in this window only, and has no local folder.
|
||||
const local = parseRemoteLocation(project.path) === null;
|
||||
// A remote project has no local folder.
|
||||
const remote = parseRemoteLocation(project.path);
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
id: "open",
|
||||
@@ -48,7 +48,17 @@ export function recentMenu(project: RecentProject, onOpened?: () => void): MenuI
|
||||
icon: "folder-open",
|
||||
run: () => void openRecent(project).then((ok) => ok && onOpened?.()),
|
||||
},
|
||||
{ kind: "action", action: "file.openInNewWindow", arg: project.path, label: "Open in New Window" },
|
||||
remote
|
||||
? {
|
||||
id: "openInNewWindow",
|
||||
label: "Open in New Window",
|
||||
icon: "folder-open",
|
||||
run: () =>
|
||||
void call(commands.windowOpenRemote(remote.sshConfig, remote.path, true))
|
||||
.then(() => onOpened?.())
|
||||
.catch(() => undefined),
|
||||
}
|
||||
: { kind: "action", action: "file.openInNewWindow", arg: project.path, label: "Open in New Window" },
|
||||
{
|
||||
id: "reveal",
|
||||
label: "Show in Finder",
|
||||
@@ -71,9 +81,7 @@ export function recentMenu(project: RecentProject, onOpened?: () => void): MenuI
|
||||
run: () => void askRemoveRecent(project),
|
||||
},
|
||||
];
|
||||
return local
|
||||
? items
|
||||
: items.filter((i) => !("id" in i && i.id === "reveal") && !("action" in i && i.action === "file.openInNewWindow"));
|
||||
return remote ? items.filter((i) => !("id" in i && i.id === "reveal")) : items;
|
||||
}
|
||||
|
||||
function relativeTime(ms: number | null): string | null {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useT, useTx } from "@/platform/i18n";
|
||||
import { isTauri } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useRemoteProgress } from "@/platform/remote";
|
||||
import { stageLabel } from "@/features/remote-dev/model";
|
||||
import { stageLabel, useConnectDialog } from "@/features/remote-dev/model";
|
||||
import { Icon, IconButton, Menu, ProgressBar, Spinner, type MenuItem, T } from "@/ui";
|
||||
import { useSettingsPages } from "@/workbench/contributions";
|
||||
import { ErrorBoundary, Slot } from "@/workbench/ErrorBoundary";
|
||||
@@ -113,7 +113,7 @@ function useFolderDrop(): boolean {
|
||||
return over;
|
||||
}
|
||||
|
||||
/** The last session's project opening at startup; a remote one shows how far connecting is. */
|
||||
/** A project opening at startup or in a new window; a remote one shows how far connecting is. */
|
||||
function Reopening({ location }: { location: string }) {
|
||||
const tx = useTx();
|
||||
const progress = useRemoteProgress((s) => s.progress);
|
||||
@@ -139,6 +139,10 @@ export function WelcomeView() {
|
||||
const t = useT();
|
||||
const version = useRecent((s) => s.version);
|
||||
const reopening = useSession((s) => s.reopening);
|
||||
// A remote project connecting with no dialog or list row to show it (a new window's).
|
||||
const connecting = useRemoteProgress((s) => s.location);
|
||||
const connectDialog = useConnectDialog((s) => s.open);
|
||||
const listOpening = useRecent((s) => s.opening);
|
||||
const opening = useProject((s) => s.opening);
|
||||
const Plugins = useSettingsPages((s) => s.pages.get(PLUGINS_PAGE)?.component);
|
||||
const [page, setPage] = useState<WelcomePage>("projects");
|
||||
@@ -150,6 +154,7 @@ export function WelcomeView() {
|
||||
}, []);
|
||||
|
||||
if (reopening) return <Reopening location={reopening} />;
|
||||
if (connecting && !connectDialog && !listOpening) return <Reopening location={connecting} />;
|
||||
|
||||
return (
|
||||
<div className={styles.welcome}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { connectRemote, forgetRemote, parseRemoteLocation, recentRemotes, remote
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { fuzzyMatch } from "@/ui";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { openRemoteProject } from "@/workbench/projectWindow";
|
||||
|
||||
interface RecentState {
|
||||
projects: RecentProject[];
|
||||
@@ -131,10 +132,11 @@ async function openRemote(
|
||||
project: Pick<RecentProject, "path" | "name">,
|
||||
remote: { sshConfig: string; path: string },
|
||||
): Promise<boolean> {
|
||||
useRecent.setState({ opening: project.path });
|
||||
try {
|
||||
await connectRemote(remote.sshConfig, remote.path);
|
||||
return true;
|
||||
return await openRemoteProject(remote.sshConfig, remote.path, async () => {
|
||||
useRecent.setState({ opening: project.path });
|
||||
await connectRemote(remote.sshConfig, remote.path);
|
||||
});
|
||||
} catch (e) {
|
||||
notifications().notify({
|
||||
level: "error",
|
||||
|
||||
@@ -4175,12 +4175,12 @@
|
||||
"Delete remote": "删除远程",
|
||||
"Delete local": "删除本地",
|
||||
"Remote Development": "远程开发",
|
||||
"The project runs on the remote host; this window shows it. The IDE backend is installed there on first use.": "项目在远程主机上运行,此窗口负责显示。首次使用时会在远程主机上安装 IDE 后端。",
|
||||
"The project runs on the remote host; the IDE backend is installed there on first use.": "项目在远程主机上运行,首次使用时会在该主机上安装 IDE 后端。",
|
||||
"SSH configuration:": "SSH 配置:",
|
||||
"No SSH configurations": "没有 SSH 配置",
|
||||
"Project directory:": "项目目录:",
|
||||
"Remote Development…": "远程开发…",
|
||||
"Opens a project folder on an SSH host in this window; the IDE backend runs on that host.": "在此窗口中打开 SSH 主机上的项目文件夹;IDE 后端在该主机上运行。",
|
||||
"Opens a project folder on an SSH host; the IDE backend runs on that host.": "打开 SSH 主机上的项目文件夹;IDE 后端在该主机上运行。",
|
||||
"Disconnect from Remote Host": "断开与远程主机的连接",
|
||||
"Opens a terminal tab connected to an SSH configuration (`arg`: its id; otherwise a chooser).": "打开连接到 SSH 配置的终端标签页(`arg`:配置 id;否则显示选择器)。",
|
||||
"Connecting to the host…": "正在连接主机…",
|
||||
|
||||
+10
-4
@@ -96,15 +96,21 @@ export async function restoreRemote(): Promise<RemoteSession | null> {
|
||||
return session;
|
||||
}
|
||||
|
||||
/** Where the connection in progress is (`null`: none). */
|
||||
export const useRemoteProgress = create<{ progress: RemoteProgress | null }>(() => ({ progress: null }));
|
||||
/** Where the connection in progress is, and to which location (`null`: none). */
|
||||
export const useRemoteProgress = create<{ progress: RemoteProgress | null; location: string | null }>(() => ({
|
||||
progress: null,
|
||||
location: null,
|
||||
}));
|
||||
|
||||
/** Opens `path` on the host of SSH configuration `sshConfig` in this window. */
|
||||
export async function connectRemote(sshConfig: string, path: string) {
|
||||
useProject.setState({ opening: true });
|
||||
const onProgress = new Channel<RemoteProgress>();
|
||||
onProgress.onmessage = (progress) => useRemoteProgress.setState({ progress });
|
||||
useRemoteProgress.setState({ progress: { stage: "connecting", fraction: null } });
|
||||
useRemoteProgress.setState({
|
||||
progress: { stage: "connecting", fraction: null },
|
||||
location: remoteLocation(sshConfig, path),
|
||||
});
|
||||
try {
|
||||
const r = await call(commands.remoteConnect(sshConfig, path, onProgress));
|
||||
enableRemote({ sshConfig: r.sshConfig, root: r.root, host: r.host, commands: r.commands });
|
||||
@@ -119,7 +125,7 @@ export async function connectRemote(sshConfig: string, path: string) {
|
||||
return r;
|
||||
} finally {
|
||||
useProject.setState({ opening: false });
|
||||
useRemoteProgress.setState({ progress: null });
|
||||
useRemoteProgress.setState({ progress: null, location: null });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/** Where a project opens when this window already has one: System Settings › Project › "Open project in". */
|
||||
import { t } from "@/platform/i18n";
|
||||
import { call, commands } from "@/platform/ipc";
|
||||
import { confirmLeaveProject, useProject } from "@/platform/project";
|
||||
import { connectRemote } from "@/platform/remote";
|
||||
import { ask } from "./messages";
|
||||
|
||||
/** "Open project in": `new` | `same` | `ask`. */
|
||||
export function openPolicy(): "ask" | "new" | "same" {
|
||||
const v = useProject.getState().settings?.general?.openProjectIn;
|
||||
return v === "new" || v === "same" ? v : "ask";
|
||||
}
|
||||
|
||||
/**
|
||||
* The window to open project `name` in: this one when it has no project, else
|
||||
* per "Open project in", asking when set to (and remembering "Don't ask again").
|
||||
* `null`: cancelled.
|
||||
*/
|
||||
export async function chooseProjectWindow(name: string): Promise<"new" | "same" | null> {
|
||||
if (!useProject.getState().project) return "same";
|
||||
const mode = openPolicy();
|
||||
if (mode !== "ask") return mode;
|
||||
const r = await ask({
|
||||
title: t("app.window.askTitle"),
|
||||
body: t("app.window.askBody", { name }),
|
||||
buttons: [
|
||||
{ id: "new", label: t("app.window.askNew") },
|
||||
{ id: "same", label: t("app.window.askThis"), primary: true },
|
||||
{ id: "cancel", label: t("common.cancel") },
|
||||
],
|
||||
checkbox: t("app.window.askRemember"),
|
||||
});
|
||||
if (r.button !== "same" && r.button !== "new") return null;
|
||||
if (r.checked) {
|
||||
try {
|
||||
useProject.setState({
|
||||
settings: await call(commands.settingsSet("global", "general.openProjectIn", JSON.stringify(r.button))),
|
||||
});
|
||||
} catch {
|
||||
/* the choice still applies to this open */
|
||||
}
|
||||
}
|
||||
return r.button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens folder `path` on the host of SSH configuration `sshConfig`: focuses the
|
||||
* window that has it, else opens it in a new window or (`here`) this one per
|
||||
* "Open project in". `false`: cancelled.
|
||||
*/
|
||||
export async function openRemoteProject(
|
||||
sshConfig: string,
|
||||
path: string,
|
||||
here: () => Promise<unknown> = () => connectRemote(sshConfig, path),
|
||||
): Promise<boolean> {
|
||||
if ((await call(commands.windowOpenRemote(sshConfig, path, false))).kind === "focused") return true;
|
||||
const mode = await chooseProjectWindow(path.replace(/\/+$/, "").split("/").pop() || path);
|
||||
if (!mode) return false;
|
||||
if (mode === "new") {
|
||||
await call(commands.windowOpenRemote(sshConfig, path, true));
|
||||
return true;
|
||||
}
|
||||
if (!(await confirmLeaveProject())) return false;
|
||||
await here();
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user