dsh-remote-vs 0.8.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +15 -2
- package/README.md +12 -1
- package/lib/client.js +1507 -59
- package/lib/types/client/CodeEditor.d.ts +27 -1
- package/lib/types/client/FloatingEditorWindow.d.ts +20 -0
- package/lib/types/client/FloatingPlaceholder.d.ts +9 -0
- package/lib/types/client/RemoteEditorView.d.ts +4 -0
- package/lib/types/client/TerminalView.d.ts +3 -0
- package/lib/types/client/XTerm.d.ts +6 -0
- package/lib/types/client/float-editor.d.ts +123 -0
- package/lib/types/client/float-tab-trigger.d.ts +24 -0
- package/lib/types/client/live-editor.d.ts +94 -0
- package/lib/types/client/remote-file-open-target.d.ts +0 -22
- package/lib/types/client/shell.d.ts +29 -0
- package/lib/types/client/store.d.ts +160 -6
- package/package.json +1 -1
- package/src/client/CodeEditor.tsx +95 -8
- package/src/client/EditorPane.tsx +45 -24
- package/src/client/FloatingEditorWindow.tsx +494 -0
- package/src/client/FloatingPlaceholder.tsx +22 -0
- package/src/client/RemoteEditorView.tsx +7 -2
- package/src/client/RemoteExplorer.tsx +61 -0
- package/src/client/RemoteRoot.tsx +16 -7
- package/src/client/SearchView.tsx +49 -2
- package/src/client/TerminalView.tsx +53 -2
- package/src/client/XTerm.tsx +15 -0
- package/src/client/float-editor.ts +271 -0
- package/src/client/float-tab-trigger.ts +51 -0
- package/src/client/i18n.ts +32 -0
- package/src/client/index.tsx +48 -1
- package/src/client/live-editor.ts +155 -0
- package/src/client/remote-file-open-target.ts +1 -5
- package/src/client/remote-file-open.ts +2 -2
- package/src/client/scm/ScmPanel.tsx +42 -1
- package/src/client/shell.ts +35 -0
- package/src/client/store.tsx +367 -16
- package/src/client/vsc/editor-theme.ts +6 -1
- package/src/client/vsc/vsc.css +235 -0
|
@@ -23,7 +23,18 @@ export interface CodeEditorViewState {
|
|
|
23
23
|
export interface CodeEditorHandle {
|
|
24
24
|
getContent: () => string;
|
|
25
25
|
isDirty: () => boolean;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Advance the dirty baseline to the bytes a save just wrote to the remote
|
|
28
|
+
* file, and re-derive the dirty flag from the LIVE buffer against them.
|
|
29
|
+
*
|
|
30
|
+
* Takes the saved text explicitly because the user may have typed while the
|
|
31
|
+
* remote write was in flight: the buffer is then NEWER than the file, and
|
|
32
|
+
* declaring it saved would be the same silent-loss bug in the other
|
|
33
|
+
* direction. `tabId` guards against the pane having switched to another tab
|
|
34
|
+
* while the save awaited — marking that editor saved would corrupt ITS
|
|
35
|
+
* baseline.
|
|
36
|
+
*/
|
|
37
|
+
markSaved: (savedText: string, tabId: string) => void;
|
|
27
38
|
/** Snapshot scroll + selection, for restoring after a remount. */
|
|
28
39
|
getViewState: () => CodeEditorViewState | undefined;
|
|
29
40
|
/** Scroll back and put the caret where it was. */
|
|
@@ -120,6 +131,21 @@ export declare const CodeEditor: import("react").ForwardRefExoticComponent<{
|
|
|
120
131
|
* where a file was when its tab is re-selected.
|
|
121
132
|
*/
|
|
122
133
|
viewState?: CodeEditorViewState;
|
|
134
|
+
/**
|
|
135
|
+
* The text as it was last LOADED from / SAVED to the remote file — the dirty
|
|
136
|
+
* baseline.
|
|
137
|
+
*
|
|
138
|
+
* Separate from `initial` because a tab switch or a surface flip remounts the
|
|
139
|
+
* editor with the LIVE buffer as `initial` (that is how unsaved edits survive
|
|
140
|
+
* the remount), while the file on disk is still the older `savedText`. Using
|
|
141
|
+
* `initial` as the baseline would declare the unsaved text clean: the dirty
|
|
142
|
+
* dot and status would lie, the close guard would stop prompting and Save
|
|
143
|
+
* would disable — a silent way to lose the very edits the handoff preserved.
|
|
144
|
+
*
|
|
145
|
+
* Absent (diff panes, older callers): the mount's own document is the
|
|
146
|
+
* baseline, which is the historical behaviour.
|
|
147
|
+
*/
|
|
148
|
+
savedText?: string;
|
|
123
149
|
onSave: () => void;
|
|
124
150
|
onDirty: (dirty: boolean) => void;
|
|
125
151
|
} & import("react").RefAttributes<CodeEditorHandle>>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The floating editor window: the plugin's ONE editor pane hosted in a
|
|
3
|
+
* draggable, resizable frame above the app shell.
|
|
4
|
+
*
|
|
5
|
+
* It is mounted by the client entry into the host's frame-wide overlay slot
|
|
6
|
+
* (`shell.overlay`) and reads the same module-level store as every other
|
|
7
|
+
* surface, so tabs / previews / diffs / find / change marks / undo / @file are
|
|
8
|
+
* not a re-implementation: this file only owns the FRAME (position, size,
|
|
9
|
+
* z-order, drag + resize + zoom) and the handoff around the mount swap.
|
|
10
|
+
*
|
|
11
|
+
* The mount is exclusive: `EditorPane` must exist exactly once, so the centre
|
|
12
|
+
* conversation view and the sidebar fallback render a placeholder while
|
|
13
|
+
* `editorSurface === 'float'` (see `FloatingEditorPlaceholder`).
|
|
14
|
+
*/
|
|
15
|
+
import { type ReactNode } from 'react';
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
17
|
+
/** The window frame plus the editor. `ctx` is only needed for the boot hook. */
|
|
18
|
+
export declare function FloatingEditorWindow(props: {
|
|
19
|
+
ctx: Context;
|
|
20
|
+
}): ReactNode;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the centre conversation view and the sidebar fallback show while the
|
|
3
|
+
* editor is popped out: the editor itself is a single instance and lives in the
|
|
4
|
+
* floating window, so these surfaces must not mount a second one (that would
|
|
5
|
+
* be two live CodeMirror buffers for one tab — the exact defect `EditorPane`'s
|
|
6
|
+
* header warns about). A dock button keeps the surface from looking broken.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReactNode } from 'react';
|
|
9
|
+
export declare function FloatingEditorPlaceholder(): ReactNode;
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* sidebar then keeps the whole column for the explorer. Both surfaces read
|
|
7
7
|
* the same module-level store, so a diff opened from source control appears
|
|
8
8
|
* here, and a save here repaints the sidebar's change list.
|
|
9
|
+
*
|
|
10
|
+
* While the editor floats (`editorSurface === 'float'`) this tab is NOT the
|
|
11
|
+
* editor's mount any more, so it renders a placeholder instead: mounting a
|
|
12
|
+
* second `EditorPane` would give the same tab two live buffers.
|
|
9
13
|
*/
|
|
10
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
15
|
import type { ReactNode } from 'react';
|
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type ReactNode } from 'react';
|
|
11
11
|
import type { RemoteHostSummary } from '../protocol.ts';
|
|
12
|
+
import type { TerminalRequest } from './store.tsx';
|
|
12
13
|
export declare function TerminalView(props: {
|
|
13
14
|
t: (key: string) => string;
|
|
14
15
|
hosts: RemoteHostSummary[];
|
|
15
16
|
visible: boolean;
|
|
17
|
+
/** A pending "open a terminal here" request from the search results' menu. */
|
|
18
|
+
request?: TerminalRequest;
|
|
16
19
|
}): ReactNode;
|
|
@@ -12,4 +12,10 @@ export declare function XTerm(props: {
|
|
|
12
12
|
fontFamily?: string;
|
|
13
13
|
/** When the parent sub-tab hides/shows this view without unmounting. */
|
|
14
14
|
visible?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Line typed into the fresh shell once its PTY reports ready (a menu entry's
|
|
17
|
+
* `cd`). Sent ONCE per mount: a reconnect is a new mount and thus a new shell,
|
|
18
|
+
* which correctly re-runs the line.
|
|
19
|
+
*/
|
|
20
|
+
initialInput?: string;
|
|
15
21
|
}): ReactNode;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry and surface rules for the floating editor.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of React and of the DOM on purpose: the drag / resize / zoom maths
|
|
5
|
+
* is the part that is easy to get subtly wrong (negative sizes, a window that
|
|
6
|
+
* escapes the viewport, a stale stored rect from a larger screen), and this
|
|
7
|
+
* module is what `scripts/check-float-editor.mjs` drives directly. The window
|
|
8
|
+
* component is then only wiring.
|
|
9
|
+
*
|
|
10
|
+
* The SURFACE rule lives here too: the plugin has exactly one live editor, and
|
|
11
|
+
* which mount owns it is a three-way decision (centre conversation view,
|
|
12
|
+
* sidebar fallback, floating window). Encoding it as a pure function keeps the
|
|
13
|
+
* invariant testable and out of three separate `=== 'center'` comparisons.
|
|
14
|
+
*/
|
|
15
|
+
/** Where the editor pane is mounted right now. */
|
|
16
|
+
export type EditorSurface = 'center' | 'sidebar' | 'float';
|
|
17
|
+
/** The persisted layout of the floating window, in viewport pixels. */
|
|
18
|
+
export interface FloatRect {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
/** Viewport size the geometry is clamped to. */
|
|
25
|
+
export interface FloatViewport {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
}
|
|
29
|
+
/** A resize grip: the compass directions of the eight handles. */
|
|
30
|
+
export type FloatResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
|
31
|
+
/** Smallest window the editor can be squeezed into and stay usable. */
|
|
32
|
+
export declare const FLOAT_MIN_WIDTH = 360;
|
|
33
|
+
export declare const FLOAT_MIN_HEIGHT = 220;
|
|
34
|
+
/** Height of the title bar; a minimized window keeps exactly this much. */
|
|
35
|
+
export declare const FLOAT_TITLEBAR_HEIGHT = 34;
|
|
36
|
+
/** Gap a maximized window keeps from the viewport edges. */
|
|
37
|
+
export declare const FLOAT_VIEWPORT_EDGE = 8;
|
|
38
|
+
/** Editor font-size range for Ctrl+wheel zoom (px). */
|
|
39
|
+
export declare const FLOAT_FONT_MIN = 9;
|
|
40
|
+
export declare const FLOAT_FONT_MAX = 28;
|
|
41
|
+
export declare const FLOAT_FONT_DEFAULT = 13;
|
|
42
|
+
/** Diameter of the minimized ball, in pixels. */
|
|
43
|
+
export declare const FLOAT_BALL_SIZE = 44;
|
|
44
|
+
/**
|
|
45
|
+
* How far a pointer may travel and still count as a CLICK: the minimized window
|
|
46
|
+
* is a single 44px ball, so "click restores / drag moves" has to be decided
|
|
47
|
+
* from one pointer gesture.
|
|
48
|
+
*/
|
|
49
|
+
export declare const FLOAT_BALL_CLICK_SLOP = 4;
|
|
50
|
+
/** A point in viewport coordinates (the minimized ball's origin). */
|
|
51
|
+
export interface FloatPoint {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
}
|
|
55
|
+
/** The effective editor surface: floating wins, otherwise the base mount. */
|
|
56
|
+
export declare function effectiveEditorSurface(base: 'center' | 'sidebar', floatOpen: boolean): EditorSurface;
|
|
57
|
+
/**
|
|
58
|
+
* The rect a fresh floating window opens at: about two thirds of the frame,
|
|
59
|
+
* centred, with a hard size cap so a 4K monitor does not produce a 2600px
|
|
60
|
+
* editor for a 40-column file.
|
|
61
|
+
*/
|
|
62
|
+
export declare function defaultFloatRect(viewport: FloatViewport): FloatRect;
|
|
63
|
+
/**
|
|
64
|
+
* Bring a rect back inside the viewport and to a legal size.
|
|
65
|
+
*
|
|
66
|
+
* Used on every drag step, on restore, and whenever the browser window is
|
|
67
|
+
* resized: a rect persisted on a 2560px screen must not open a window whose
|
|
68
|
+
* title bar is off-screen on a 1366px laptop.
|
|
69
|
+
*/
|
|
70
|
+
export declare function clampFloatRect(rect: FloatRect, viewport: FloatViewport): FloatRect;
|
|
71
|
+
/** Move the window by a pointer delta, staying inside the viewport. */
|
|
72
|
+
export declare function dragFloatRect(start: FloatRect, dx: number, dy: number, viewport: FloatViewport): FloatRect;
|
|
73
|
+
/**
|
|
74
|
+
* Resize from one grip.
|
|
75
|
+
*
|
|
76
|
+
* Each axis is adjusted from the edge that MOVED (west/north resize changes the
|
|
77
|
+
* origin too), then clamped: the minimum size is anchored at the opposite edge
|
|
78
|
+
* (so dragging the north grip past the bottom stops instead of flipping the
|
|
79
|
+
* rect) and no edge may leave the viewport.
|
|
80
|
+
*/
|
|
81
|
+
export declare function resizeFloatRect(start: FloatRect, handle: FloatResizeHandle, dx: number, dy: number, viewport: FloatViewport): FloatRect;
|
|
82
|
+
/** The rect a maximized window occupies: the frame minus a small margin. */
|
|
83
|
+
export declare function maximizeFloatRect(viewport: FloatViewport): FloatRect;
|
|
84
|
+
/**
|
|
85
|
+
* Parse a persisted rect.
|
|
86
|
+
*
|
|
87
|
+
* Anything malformed is discarded rather than repaired into a guess: the
|
|
88
|
+
* caller then opens the default rect. Values that are merely STALE (a rect from
|
|
89
|
+
* another screen) are legal input and get clamped.
|
|
90
|
+
*/
|
|
91
|
+
export declare function parseFloatRect(raw: string | null | undefined, viewport: FloatViewport): FloatRect | undefined;
|
|
92
|
+
/** Serialize a rect for localStorage. */
|
|
93
|
+
export declare function serializeFloatRect(rect: FloatRect): string;
|
|
94
|
+
/**
|
|
95
|
+
* One Ctrl+wheel step: up (negative deltaY) grows the text, down shrinks it.
|
|
96
|
+
* Values are whole pixels so the stored size stays stable across sessions.
|
|
97
|
+
*/
|
|
98
|
+
export declare function stepFloatZoom(current: number, deltaY: number): number;
|
|
99
|
+
/** Parse the persisted editor font size (fallback = the default 13px). */
|
|
100
|
+
export declare function parseFloatFontSize(raw: string | null | undefined): number;
|
|
101
|
+
/**
|
|
102
|
+
* Where the minimized ball may sit: fully on screen, whatever position was
|
|
103
|
+
* asked for (a ball restored on a smaller monitor must stay clickable).
|
|
104
|
+
*/
|
|
105
|
+
export declare function clampBallPosition(pos: FloatPoint, viewport: FloatViewport): FloatPoint;
|
|
106
|
+
/**
|
|
107
|
+
* The ball's default spot when a window is minimized: its own top-left corner,
|
|
108
|
+
* so the ball appears where the window was instead of jumping to a corner.
|
|
109
|
+
*/
|
|
110
|
+
export declare function ballFromRect(rect: FloatRect, viewport: FloatViewport): FloatPoint;
|
|
111
|
+
/**
|
|
112
|
+
* Whether a finished gesture was a CLICK rather than a drag.
|
|
113
|
+
*
|
|
114
|
+
* The minimized window is one small target with two actions on the same
|
|
115
|
+
* pointer: a press that never really moved restores the window, a real drag
|
|
116
|
+
* moves the ball. Without this distinction every move-by-3px would also
|
|
117
|
+
* restore, and every click would be a zero-distance drag.
|
|
118
|
+
*/
|
|
119
|
+
export declare function isBallClick(dx: number, dy: number): boolean;
|
|
120
|
+
/** Parse a persisted ball position (stale geometry is clamped, garbage dropped). */
|
|
121
|
+
export declare function parseBallPosition(raw: string | null | undefined, viewport: FloatViewport): FloatPoint | undefined;
|
|
122
|
+
/** Serialize a ball position for localStorage. */
|
|
123
|
+
export declare function serializeBallPosition(pos: FloatPoint): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Double-click on the shell's 「远程编辑器」 tab floats the editor.
|
|
3
|
+
*
|
|
4
|
+
* That tab belongs to the host's conversation-view header, not to us: the plugin
|
|
5
|
+
* registers a VIEW into the shell's tab strip and never sees the tab row's DOM.
|
|
6
|
+
* There is no public service for "the user double-clicked my tab" either, so the
|
|
7
|
+
* only way to honour the gesture is a document-level `dblclick` listener that
|
|
8
|
+
* recognises the tab the same way `reveal.ts` does for clicks — by its label,
|
|
9
|
+
* skipping our own surfaces.
|
|
10
|
+
*
|
|
11
|
+
* Fail-safe by construction: a miss (another host build, a renamed tab, a
|
|
12
|
+
* double-click somewhere else) simply does nothing. It never prevents the
|
|
13
|
+
* default action, never throws, and does not steal double-clicks from the
|
|
14
|
+
* plugin's own UI (`.rvs-root`) or from dialogs.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Install the double-click trigger.
|
|
18
|
+
*
|
|
19
|
+
* @param label - the tab's visible label (the same localized string the view
|
|
20
|
+
* registered with); matched against the tab's trimmed text.
|
|
21
|
+
* @param onTrigger - called once per recognised double-click.
|
|
22
|
+
* @returns disposer removing the listener.
|
|
23
|
+
*/
|
|
24
|
+
export declare function installFloatTabTrigger(label: string, onTrigger: () => void): () => void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handoff for the ONE live editor buffer when its mount is replaced.
|
|
3
|
+
*
|
|
4
|
+
* The plugin mounts exactly one `EditorPane` at a time (centre conversation
|
|
5
|
+
* view, sidebar fallback, or the floating window), and `CodeEditor` reads its
|
|
6
|
+
* `initial` text once on mount. So moving the editor between surfaces is
|
|
7
|
+
* effectively a remount: without a handoff the buffer typed since the last
|
|
8
|
+
* load is gone and the tab's stored text is the pre-edit one.
|
|
9
|
+
*
|
|
10
|
+
* This module is the handoff, and it is module-level for the same reason the
|
|
11
|
+
* store is: `EditorPane` is unmounted before the replacement mounts, so no
|
|
12
|
+
* React state survives the swap.
|
|
13
|
+
*
|
|
14
|
+
* Two things travel, and they are captured SEPARATELY because their costs
|
|
15
|
+
* differ by orders of magnitude:
|
|
16
|
+
* - the buffer text (the store copies it onto the tab before the surface
|
|
17
|
+
* flips, so the replacement mounts with it as `initial`) — materializes the
|
|
18
|
+
* whole document, so it is taken only when the tab may differ from disk;
|
|
19
|
+
* - scroll + caret (kept here and read back by the replacement, which is how
|
|
20
|
+
* a tab switch already restored its position — same storage, now shared by
|
|
21
|
+
* every surface instead of one component's `useRef`) — cheap, taken on
|
|
22
|
+
* every switch.
|
|
23
|
+
*/
|
|
24
|
+
import type { CodeEditorHandle, CodeEditorViewState } from './CodeEditor.tsx';
|
|
25
|
+
/**
|
|
26
|
+
* Register the editor that is on screen right now.
|
|
27
|
+
*
|
|
28
|
+
* The caller passes a getter, not a handle: `editorRef.current` is `null`
|
|
29
|
+
* during the first render and is cleared before the parent's cleanup runs, so
|
|
30
|
+
* a snapshot taken later must read through the getter.
|
|
31
|
+
* @returns disposer; only the registration that is still current is cleared.
|
|
32
|
+
*/
|
|
33
|
+
export declare function registerLiveEditor(tabId: string, getHandle: () => CodeEditorHandle | null): () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Snapshot the live position (scroll + caret) without touching the document.
|
|
36
|
+
*
|
|
37
|
+
* Cheap, so the store can call it on EVERY switch: a surface flip remounts the
|
|
38
|
+
* very same tab, and the replacement's render happens before the old view's
|
|
39
|
+
* unmount report — the position has to be in the map before that render.
|
|
40
|
+
*/
|
|
41
|
+
export declare function captureLiveEditorPosition(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the live buffer differs from its disk baseline.
|
|
44
|
+
*
|
|
45
|
+
* The store uses this to skip the O(document) text copy on clean tabs (see
|
|
46
|
+
* `captureLiveBuffer`): the boolean is cheap, `getContent()` materializes the
|
|
47
|
+
* whole file as a string.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isLiveEditorDirty(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Copy the live buffer's text, for a switch that would otherwise lose it.
|
|
52
|
+
*
|
|
53
|
+
* Called from the store BEFORE the tab switch / surface flip: at that moment
|
|
54
|
+
* the editor is still mounted and its CodeMirror view still owns the edits.
|
|
55
|
+
* @returns undefined when no file editor is live (a diff tab, or a view that
|
|
56
|
+
* was never the selected one) — there is nothing to carry over then.
|
|
57
|
+
*/
|
|
58
|
+
export declare function captureLiveEditorText(): {
|
|
59
|
+
tabId: string;
|
|
60
|
+
text: string;
|
|
61
|
+
} | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Whether one tab's file editor is the one registered right now.
|
|
64
|
+
*
|
|
65
|
+
* The save path asks this before it clears a tab's dirty flag: if an editor IS
|
|
66
|
+
* mounted for that tab, the flag belongs to the live buffer and only the
|
|
67
|
+
* editor may clear it (see `markLiveEditorSaved`).
|
|
68
|
+
*/
|
|
69
|
+
export declare function isLiveEditorFor(tabId: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Advance the registered editor's dirty baseline to the bytes a save wrote.
|
|
72
|
+
*
|
|
73
|
+
* Routed through the MODULE-LEVEL registration rather than a React ref on
|
|
74
|
+
* purpose: the pane can be remounted while the remote write is in flight (a
|
|
75
|
+
* surface flip, a dock, a tab switch), and the ref the save started with is
|
|
76
|
+
* then detached — that is exactly how a tab ended up reported clean while its
|
|
77
|
+
* live buffer was newer than the file. The registration always names the editor
|
|
78
|
+
* mounted NOW, so the correction lands on whichever instance is on screen, and
|
|
79
|
+
* it is skipped (never misapplied) when that editor shows another tab: that tab
|
|
80
|
+
* heals on its next mount from `savedText` (see `CodeEditor`).
|
|
81
|
+
* @returns true when an editor for this tab took the new baseline.
|
|
82
|
+
*/
|
|
83
|
+
export declare function markLiveEditorSaved(tabId: string, savedText: string): boolean;
|
|
84
|
+
/** Remember one tab's scroll + caret (reported by the editor, see EditorPane). */
|
|
85
|
+
export declare function rememberEditorViewState(tabId: string, state: CodeEditorViewState): void;
|
|
86
|
+
/** The stored position for one tab, or undefined when it was never seen. */
|
|
87
|
+
export declare function editorViewStateOf(tabId: string): CodeEditorViewState | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Drop positions of tabs that no longer exist.
|
|
90
|
+
*
|
|
91
|
+
* The map used to live in a component with the same cleanup: without it a
|
|
92
|
+
* long session accumulated one state object per file ever opened.
|
|
93
|
+
*/
|
|
94
|
+
export declare function pruneEditorViewStates(liveIds: ReadonlySet<string>): void;
|
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Where a clicked file reference should open when the owning session lives in
|
|
3
|
-
* a remote workspace (pure — no React, no fetch, so the gate can drive it).
|
|
4
|
-
*
|
|
5
|
-
* DSH renders every file reference as a `dsh-resource://file/…` address and
|
|
6
|
-
* opens it through its own right-sidebar preview. That preview resolves the
|
|
7
|
-
* address path against the SESSION WORKSPACE ROOT. A model that quotes a path
|
|
8
|
-
* relative to its CURRENT directory (e.g. `_xfer/reports/x.md` while working
|
|
9
|
-
* in `<workspace>/<inner-repo>`) therefore misses by one segment, and a
|
|
10
|
-
* remote session's workspace root only exists as an EMPTY local alias dir —
|
|
11
|
-
* the click dies with the host-side ENOENT the user sees.
|
|
12
|
-
*
|
|
13
|
-
* This module answers "which binding, which remote paths to try" for the
|
|
14
|
-
* client interceptor:
|
|
15
|
-
* - relative paths are tried against the workspace root first and the
|
|
16
|
-
* session's current working directory second (the model's usual writing
|
|
17
|
-
* base), which is what makes the one-segment miss heal;
|
|
18
|
-
* - absolute spellings (local alias, POSIX remote, drive-qualified alias)
|
|
19
|
-
* are folded back onto the binding;
|
|
20
|
-
* - anything that does not name a known binding answers `undefined`, so the
|
|
21
|
-
* native preview keeps handling local sessions and unknown addresses.
|
|
22
|
-
*/
|
|
23
1
|
/** One remote binding as the client store exposes it. */
|
|
24
2
|
export interface RemoteOpenBinding {
|
|
25
3
|
id: string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell quoting for the lines this plugin types into a remote terminal.
|
|
3
|
+
*
|
|
4
|
+
* One rule, one place: the client builds POSIX shell text in three spots (the
|
|
5
|
+
* `cd` typed into a fresh terminal, the bounded suffix search for an unresolved
|
|
6
|
+
* click target, and the terminal command a menu entry sends), and a second copy
|
|
7
|
+
* of the quoting rule is how an injection bug reappears after it was fixed once.
|
|
8
|
+
*
|
|
9
|
+
* Single quotes are the only form that is safe for arbitrary paths — double
|
|
10
|
+
* quotes still expand `$`, backticks and `\` — and the closing quote is the one
|
|
11
|
+
* character they cannot carry, so it is spliced in as `'\''`.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Quote one value for a POSIX shell, as a single word.
|
|
15
|
+
*
|
|
16
|
+
* @param value - the raw path or word.
|
|
17
|
+
* @returns `'value'` with every embedded `'` spliced as `'\''`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function shellQuote(value: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* The command typed into a fresh remote terminal to land in one directory.
|
|
22
|
+
*
|
|
23
|
+
* A trailing newline is NOT included: the caller writes the line and the
|
|
24
|
+
* terminal's Enter, which keeps the same string reusable for display.
|
|
25
|
+
*
|
|
26
|
+
* @param dir - absolute remote directory.
|
|
27
|
+
* @returns `cd '<dir>'`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function cdCommand(dir: string): string;
|
|
@@ -5,6 +5,7 @@ import { type StatusBuckets, type TreeMarks } from './scm/status-map.ts';
|
|
|
5
5
|
import { type DiffPayload } from './scm/actions.ts';
|
|
6
6
|
import { type DirExpansions } from './scm/untracked-dirs.ts';
|
|
7
7
|
import { type Lang } from './i18n.ts';
|
|
8
|
+
import { type EditorSurface } from './float-editor.ts';
|
|
8
9
|
/** One editor tab (code file, preview or diff). */
|
|
9
10
|
export interface EditorTab {
|
|
10
11
|
id: string;
|
|
@@ -17,6 +18,16 @@ export interface EditorTab {
|
|
|
17
18
|
error: string | undefined;
|
|
18
19
|
/** File payload. */
|
|
19
20
|
text: string;
|
|
21
|
+
/**
|
|
22
|
+
* The text as last LOADED from / SAVED to the remote file.
|
|
23
|
+
*
|
|
24
|
+
* `text` is the live-buffer handoff copy (see `captureLiveBuffer`) and may
|
|
25
|
+
* therefore hold unsaved edits; this is the disk-side reference the editor
|
|
26
|
+
* uses as its dirty baseline. Without the split, a remount with the handoff
|
|
27
|
+
* text would label unsaved edits "clean" (dirty dot off, Save disabled, close
|
|
28
|
+
* guard silent) — the exact silent-loss path the handoff exists to avoid.
|
|
29
|
+
*/
|
|
30
|
+
baseline?: string;
|
|
20
31
|
dirty: boolean;
|
|
21
32
|
saving: boolean;
|
|
22
33
|
binary: boolean;
|
|
@@ -82,6 +93,30 @@ export interface SearchRequest {
|
|
|
82
93
|
/** Absolute remote directory the search is scoped to. */
|
|
83
94
|
scope: string;
|
|
84
95
|
}
|
|
96
|
+
/** One "show this folder in the explorer tree" request. */
|
|
97
|
+
export interface RevealRequest {
|
|
98
|
+
/** Monotonic id so asking for the same folder twice still re-triggers. */
|
|
99
|
+
id: number;
|
|
100
|
+
/** Absolute remote DIRECTORY to expand and scroll to. */
|
|
101
|
+
path: string;
|
|
102
|
+
}
|
|
103
|
+
/** One "open a terminal here" request. */
|
|
104
|
+
export interface TerminalRequest {
|
|
105
|
+
/** Monotonic id so the same folder can be opened twice. */
|
|
106
|
+
id: number;
|
|
107
|
+
/**
|
|
108
|
+
* Host alias the directory lives on.
|
|
109
|
+
*
|
|
110
|
+
* Carried explicitly, and NOT taken from the terminal panel's own last-used
|
|
111
|
+
* host: the panel remembers whatever host the user ran a shell on last, so a
|
|
112
|
+
* menu action on a workspace bound elsewhere opened the shell on the WRONG
|
|
113
|
+
* machine — the `cd` then failed with "No such file or directory" for a path
|
|
114
|
+
* that exists perfectly well on its own host.
|
|
115
|
+
*/
|
|
116
|
+
alias: string;
|
|
117
|
+
/** Absolute remote directory the fresh shell should `cd` into. */
|
|
118
|
+
dir: string;
|
|
119
|
+
}
|
|
85
120
|
/** Stored repository sub-directory for one binding ('' = the workspace root). */
|
|
86
121
|
export declare function readRepoSubdir(workspaceId: string): string;
|
|
87
122
|
/** Everything a view can read, plus the actions. */
|
|
@@ -120,9 +155,16 @@ export interface ExplorerValue {
|
|
|
120
155
|
activeTab: EditorTab | undefined;
|
|
121
156
|
activeTabId: string | undefined;
|
|
122
157
|
revision: number;
|
|
123
|
-
|
|
158
|
+
/** Effective editor mount: the floating window wins over the base surface. */
|
|
159
|
+
editorSurface: EditorSurface;
|
|
160
|
+
floatOpen: boolean;
|
|
161
|
+
floatAvailable: boolean;
|
|
124
162
|
view: RailView;
|
|
125
163
|
searchRequest: SearchRequest | undefined;
|
|
164
|
+
/** Pending "show this folder in the tree" request (consumed by the explorer). */
|
|
165
|
+
revealRequest: RevealRequest | undefined;
|
|
166
|
+
/** Pending "open a terminal here" request (consumed by the terminal panel). */
|
|
167
|
+
terminalRequest: TerminalRequest | undefined;
|
|
126
168
|
setView: (view: RailView) => void;
|
|
127
169
|
/** Switch to the search panel (optionally scoped to a folder) — the tree's
|
|
128
170
|
"find in folder" entry point. */
|
|
@@ -131,6 +173,17 @@ export interface ExplorerValue {
|
|
|
131
173
|
query?: string;
|
|
132
174
|
scope: string;
|
|
133
175
|
}) => void;
|
|
176
|
+
/**
|
|
177
|
+
* Show one remote directory in the explorer: switch to it, expand the folder
|
|
178
|
+
* and its ancestors, then scroll its row into view. Raised by the search
|
|
179
|
+
* results' "open containing folder" entry.
|
|
180
|
+
*/
|
|
181
|
+
revealInExplorer: (dir: string) => void;
|
|
182
|
+
/**
|
|
183
|
+
* Open a fresh terminal tab whose shell `cd`s into `dir`. Raised by the
|
|
184
|
+
* search results' "open in terminal" entry.
|
|
185
|
+
*/
|
|
186
|
+
openTerminalAt: (dir: string) => void;
|
|
134
187
|
/**
|
|
135
188
|
* The USER's pick — the workspace dropdowns and the wizard's "open files".
|
|
136
189
|
*
|
|
@@ -160,6 +213,21 @@ export interface ExplorerValue {
|
|
|
160
213
|
openFile: (fullPath: string, options?: {
|
|
161
214
|
line?: number;
|
|
162
215
|
}) => void;
|
|
216
|
+
/**
|
|
217
|
+
* 「在悬浮编辑器中打开」:把编辑器整体浮出,再打开这个文件。
|
|
218
|
+
*
|
|
219
|
+
* 先捕获当前编辑器缓冲区再切表面(见 `captureLiveBuffer`):主界面里没保存
|
|
220
|
+
* 的编辑不会因为这次切换丢掉。
|
|
221
|
+
*/
|
|
222
|
+
openInFloatEditor: (fullPath: string, options?: {
|
|
223
|
+
line?: number;
|
|
224
|
+
}) => void;
|
|
225
|
+
/** 同上,只是打开的是工作区 diff(SCM 更改列表的悬浮入口)。 */
|
|
226
|
+
openDiffInFloatEditor: (rel: string, staged: boolean, untracked?: boolean) => void;
|
|
227
|
+
/** 把当前编辑器整体浮出(双击会话视图的「远程编辑器」标签)。 */
|
|
228
|
+
openFloatingEditor: () => void;
|
|
229
|
+
/** 停靠回主界面(底层的中心视图或侧栏表面)。 */
|
|
230
|
+
dockFloatingEditor: () => void;
|
|
163
231
|
openDiff: (rel: string, staged: boolean, untracked?: boolean) => void;
|
|
164
232
|
openCommitFile: (hash: string, rel: string) => void;
|
|
165
233
|
activateTab: (id: string) => void;
|
|
@@ -176,7 +244,12 @@ export interface ExplorerValue {
|
|
|
176
244
|
* `changeSet.marks`,`hunk` 是 `patches` 的下标)。
|
|
177
245
|
*/
|
|
178
246
|
undoChange: (tabId: string, mark: ChangeMark) => Promise<void>;
|
|
179
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Write one file tab back to the remote host.
|
|
249
|
+
* @returns true only when the write really succeeded — the caller uses it to
|
|
250
|
+
* advance the editor's dirty baseline (see `CodeEditorHandle.markSaved`).
|
|
251
|
+
*/
|
|
252
|
+
saveTab: (id: string, content: string) => Promise<boolean>;
|
|
180
253
|
reloadTab: (id: string) => Promise<void>;
|
|
181
254
|
info: (text: string) => void;
|
|
182
255
|
fail: (text: string) => void;
|
|
@@ -197,6 +270,32 @@ export declare function requestSearch(request: {
|
|
|
197
270
|
query?: string;
|
|
198
271
|
scope: string;
|
|
199
272
|
}): void;
|
|
273
|
+
/**
|
|
274
|
+
* Show one remote directory in the explorer.
|
|
275
|
+
*
|
|
276
|
+
* Two stages, because the tree is lazily loaded: this raises the request and
|
|
277
|
+
* switches the panel, and the explorer itself expands the ancestors (loading
|
|
278
|
+
* each level) and scrolls the row into view — it is the only place that knows
|
|
279
|
+
* which directories are currently expanded.
|
|
280
|
+
*
|
|
281
|
+
* @param dir - absolute remote DIRECTORY (a file's parent for a file hit).
|
|
282
|
+
*/
|
|
283
|
+
export declare function revealInExplorer(dir: string): void;
|
|
284
|
+
/**
|
|
285
|
+
* Open a fresh terminal tab whose shell lands in `dir` — on the CURRENT
|
|
286
|
+
* workspace's host.
|
|
287
|
+
*
|
|
288
|
+
* The terminal panel consumes the request (it owns the tabs) and the shell gets
|
|
289
|
+
* a quoted `cd` once its PTY reports ready — the host's terminal route has no
|
|
290
|
+
* "start in this directory" parameter, so a typed command is the honest way.
|
|
291
|
+
*
|
|
292
|
+
* The alias travels IN the request. The panel otherwise opens new tabs on the
|
|
293
|
+
* host it last used, which put the shell on a different machine than the path
|
|
294
|
+
* belongs to and made the `cd` fail with "No such file or directory".
|
|
295
|
+
*
|
|
296
|
+
* @param dir - absolute remote directory.
|
|
297
|
+
*/
|
|
298
|
+
export declare function openTerminalAt(dir: string): void;
|
|
200
299
|
/**
|
|
201
300
|
* Select the binding every view works against — the USER's choice.
|
|
202
301
|
*
|
|
@@ -373,8 +472,16 @@ export declare function remoteOpenBindings(): {
|
|
|
373
472
|
export declare function openRemoteFileInBinding(bindingId: string, remotePath: string, options?: {
|
|
374
473
|
line?: number;
|
|
375
474
|
}): boolean;
|
|
376
|
-
/**
|
|
377
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Whether the plugin's own rich editor is the place a remote file click should
|
|
477
|
+
* land (the click interceptor's gate, see `remote-file-open.ts`).
|
|
478
|
+
*
|
|
479
|
+
* The centre conversation view is the normal case; while the editor floats it
|
|
480
|
+
* is the floating window instead — the file still belongs in OUR editor, not in
|
|
481
|
+
* the host's native preview. On the sidebar-fallback layout (`sidebar`) the
|
|
482
|
+
* interceptor stays off, exactly as before this feature.
|
|
483
|
+
*/
|
|
484
|
+
export declare function isEditorTakeoverActive(): boolean;
|
|
378
485
|
/** Open (or focus) a working-tree diff of one repository path. */
|
|
379
486
|
export declare function openDiff(rel: string, staged: boolean, untracked?: boolean): void;
|
|
380
487
|
/** Open (or focus) one file of one commit (`ref^` against `ref`). */
|
|
@@ -382,8 +489,8 @@ export declare function openCommitFile(hash: string, rel: string): void;
|
|
|
382
489
|
export declare function activateTab(id: string): void;
|
|
383
490
|
export declare function closeTab(id: string): void;
|
|
384
491
|
export declare function updateTab(id: string, next: Partial<EditorTab>): void;
|
|
385
|
-
/** Write one tab back to the remote host. */
|
|
386
|
-
export declare function saveTab(id: string, content: string): Promise<
|
|
492
|
+
/** Write one tab back to the remote host. @returns true when the write landed. */
|
|
493
|
+
export declare function saveTab(id: string, content: string): Promise<boolean>;
|
|
387
494
|
/**
|
|
388
495
|
* Re-read one tab (file content or diff payload).
|
|
389
496
|
*
|
|
@@ -405,6 +512,53 @@ export declare function saveTab(id: string, content: string): Promise<void>;
|
|
|
405
512
|
export declare function reloadTab(id: string): Promise<void>;
|
|
406
513
|
/** Note where the editor pane is mounted (set by the client entry). */
|
|
407
514
|
export declare function setEditorSurface(surface: 'center' | 'sidebar'): void;
|
|
515
|
+
/**
|
|
516
|
+
* Record that the host's frame-wide overlay slot actually rendered the
|
|
517
|
+
* floating window host (see `FloatingEditorWindow`).
|
|
518
|
+
*
|
|
519
|
+
* This is a runtime handshake, not a configuration flag: on a host whose
|
|
520
|
+
* `shell.overlay` slot is missing, the entry never mounts and the menu entries
|
|
521
|
+
* stay disabled instead of opening a surface that cannot paint.
|
|
522
|
+
*/
|
|
523
|
+
export declare function setFloatEditorAvailable(available: boolean): void;
|
|
524
|
+
/**
|
|
525
|
+
* Float the editor in its current state, without opening anything.
|
|
526
|
+
*
|
|
527
|
+
* The entry is a double-click on the shell's 「远程编辑器」 tab (see
|
|
528
|
+
* `float-tab-trigger.ts`): the user is already looking at the editor and wants
|
|
529
|
+
* it out of the layout — no file, no diff, same tabs and buffers.
|
|
530
|
+
*
|
|
531
|
+
* Idempotent: while the editor already floats this does nothing (the window is
|
|
532
|
+
* on screen; a second trigger must not restart it).
|
|
533
|
+
*/
|
|
534
|
+
export declare function openFloatingEditor(): void;
|
|
535
|
+
/**
|
|
536
|
+
* Open one file in the floating editor (file-tree / search / SCM menu action).
|
|
537
|
+
*
|
|
538
|
+
* Order matters twice:
|
|
539
|
+
* 1. the live buffer is captured BEFORE the surface flips (the flip unmounts
|
|
540
|
+
* the editor that owns it);
|
|
541
|
+
* 2. a second capture runs even when already floating, because opening
|
|
542
|
+
* another file is a tab switch — the very action that remounts the editor
|
|
543
|
+
* and would otherwise drop the edits typed in the tab being left.
|
|
544
|
+
* A host whose overlay slot is unavailable gets the file in the normal surface
|
|
545
|
+
* instead (with the reason toasted), so the click is never a dead end.
|
|
546
|
+
*/
|
|
547
|
+
export declare function openInFloatEditor(fullPath: string, options?: {
|
|
548
|
+
line?: number;
|
|
549
|
+
}): void;
|
|
550
|
+
/**
|
|
551
|
+
* The floating variant of `openDiff` — the SCM change list's entry.
|
|
552
|
+
*
|
|
553
|
+
* Same handoff order as `openInFloatEditor`; a diff tab has no text buffer of
|
|
554
|
+
* its own, but the file editor that WAS on screen still does.
|
|
555
|
+
*/
|
|
556
|
+
export declare function openDiffInFloatEditor(rel: string, staged: boolean, untracked?: boolean): void;
|
|
557
|
+
/**
|
|
558
|
+
* Dock the editor back onto its base surface (centre conversation view or the
|
|
559
|
+
* sidebar fallback), carrying the live buffer with it.
|
|
560
|
+
*/
|
|
561
|
+
export declare function dockFloatingEditor(): void;
|
|
408
562
|
/**
|
|
409
563
|
* 换一份「本会话改了什么」的标记。
|
|
410
564
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-vs",
|
|
3
3
|
"description": "VSCode-style remote development for DSH: SSH host manager (key/password), helper deploy pipeline with version check, remote file explorer + editor + terminal in dsh-better-sidebar.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|