dsh-vscode-mode 0.12.0 → 0.13.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.md +129 -103
- package/assets/shell/dsh-open.cs +65 -149
- package/assets/shell/dsh-open.mjs +241 -0
- package/assets/shell/dsh-open.ps1 +28 -47
- package/assets/shell/dsh-open.sh +25 -66
- package/lib/client.js +12465 -11526
- package/lib/client.js.map +1 -1
- package/lib/index.js +3889 -2336
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
- package/src/childEnv.ts +19 -0
- package/src/client/addToConversation.ts +4 -2
- package/src/client/ai/inlineProvider.ts +1 -8
- package/src/client/commandBridge.ts +6 -2
- package/src/client/externalOpen.ts +40 -19
- package/src/client/index.ts +10 -4
- package/src/client/keybindings.ts +18 -1
- package/src/client/monaco/loader.ts +3 -1
- package/src/client/openFlow.ts +36 -28
- package/src/client/openReceipt.ts +64 -0
- package/src/client/panelCommands.ts +101 -0
- package/src/client/pdf/pdfLoader.ts +4 -2
- package/src/client/receiptState.ts +120 -0
- package/src/client/rpc.ts +30 -4
- package/src/client/shortcutRecorder.ts +161 -0
- package/src/client/shortcutsOfficial.ts +13 -4
- package/src/client/sidebarBridge.ts +7 -0
- package/src/client/ui/DiffBadge.ts +2 -1
- package/src/client/ui/EditorView.ts +237 -55
- package/src/client/ui/McpSettings.ts +6 -3
- package/src/client/ui/OfficialSideTab.ts +5 -2
- package/src/client/ui/QuickOpen.ts +10 -9
- package/src/client/ui/ShortcutSettings.ts +34 -54
- package/src/client/ui/commandCatalog.ts +5 -4
- package/src/client/useOpenReceipt.ts +128 -0
- package/src/compat.ts +5 -3
- package/src/dap/manager.ts +46 -13
- package/src/devForm.ts +252 -92
- package/src/externalHandoff.ts +93 -52
- package/src/fileOpenSettings.ts +1 -24
- package/src/hostImport.ts +19 -0
- package/src/httpGuard.ts +101 -0
- package/src/index.ts +106 -97
- package/src/integrate.ts +56 -50
- package/src/lsp/transport.ts +84 -127
- package/src/mcp.ts +58 -9
- package/src/mcpIsolation.ts +211 -102
- package/src/mcpProject.ts +220 -133
- package/src/mcpRuntime.ts +343 -0
- package/src/openBridge.ts +69 -0
- package/src/openInbox.ts +207 -0
- package/src/paths.ts +8 -5
- package/src/processTree.ts +63 -0
- package/src/reveal.ts +5 -3
- package/src/routes.ts +156 -154
- package/src/rpc.ts +14 -13
- package/src/shared/appUrl.ts +19 -0
- package/src/shared/externalOpen.ts +71 -0
- package/src/shared/integration.ts +2 -0
- package/src/shared/keybindings.ts +8 -5
- package/src/shared/mcp.ts +4 -1
- package/src/shared/rpc.ts +7 -4
- package/src/shared/svn.ts +6 -8
- package/src/svnPlanTool.ts +51 -0
- package/src/unityBridge.ts +26 -3
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
- package/unity/com.dsh.editor/README.md +16 -25
- package/unity/com.dsh.editor/package.json +2 -2
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// OpenProject:优先把打开请求移交 host(edrv.external.handoff)——已打开的 DSH 页面
|
|
5
|
-
// (3s 移交轮询)就地执行打开规则,不重复开新页;无活跃页面/2s 未领取(页面刚关)则
|
|
6
|
-
// 回退 Application.OpenURL 深链。移交全程后台线程(不阻塞编辑器主线程),
|
|
7
|
-
// 回退开浏览器经 EditorApplication.delayCall 切回主线程。
|
|
8
|
-
// 深链契约见插件 src/shared/externalOpen.ts(edrvOpen/edrvPaths/edrvLine/edrvColumn)。
|
|
9
|
-
// 过滤:Unity 对双击的任何资产(含 prefab/scene)都会回调 OpenProject;仅放行文本/代码类
|
|
10
|
-
// 扩展名(白名单 + Project Settings 用户扩展),其余返回 false 交还 Unity 原生处理
|
|
11
|
-
// (双击预制体进预制体模式、双击场景开场景),对齐 DefaultExternalCodeEditor 行为。
|
|
12
|
-
// 作者 ddj 2026-09-08
|
|
1
|
+
// DSH external editor: current-user private OPEN REQUEST/ACK queue.
|
|
2
|
+
// Unity API and preferences stay on the main thread; the worker uses the installed producer.
|
|
3
|
+
// @author ddj 2026-09-28
|
|
13
4
|
using System;
|
|
14
5
|
using System.IO;
|
|
15
|
-
using System.Net;
|
|
16
|
-
using System.Text;
|
|
17
6
|
using System.Threading;
|
|
18
7
|
using UnityEditor;
|
|
19
8
|
using UnityEngine;
|
|
@@ -26,11 +15,10 @@ namespace Dsh.EditorIntegration
|
|
|
26
15
|
{
|
|
27
16
|
private const string EditorName = "DSH 文件编辑";
|
|
28
17
|
private const string VirtualPath = "dsh-editor://vscode-mode";
|
|
29
|
-
private const string
|
|
30
|
-
private const string DefaultBaseUrl = "http://127.0.0.1:3080";
|
|
31
|
-
private const string DshVersion = "0.2.4";
|
|
18
|
+
private const string DshVersion = "0.3.0";
|
|
32
19
|
internal const string VersionText = DshVersion;
|
|
33
20
|
|
|
21
|
+
/// <summary>Register this editor without changing the user's selection. @author ddj 2026-09-28</summary>
|
|
34
22
|
static DshCodeEditor()
|
|
35
23
|
{
|
|
36
24
|
try
|
|
@@ -38,20 +26,16 @@ namespace Dsh.EditorIntegration
|
|
|
38
26
|
CodeEditor.Register(new DshCodeEditor());
|
|
39
27
|
Debug.Log("[DshCodeEditor] 已注册为 Unity 外部脚本编辑器(v" + DshVersion + ")");
|
|
40
28
|
}
|
|
41
|
-
catch (Exception
|
|
42
|
-
{
|
|
43
|
-
Debug.LogError("[DshCodeEditor] 注册失败:" + e);
|
|
44
|
-
}
|
|
29
|
+
catch (Exception error) { Debug.LogError("[DshCodeEditor] 注册失败:" + error); }
|
|
45
30
|
}
|
|
46
31
|
|
|
47
32
|
public CodeEditor.Installation[] Installations
|
|
48
33
|
{
|
|
49
|
-
// Path 指向真实存在的 Unity.exe:部分 Unity 版本会过滤「路径不存在」的安装条目;
|
|
50
|
-
// 打开逻辑全在本类(OpenProject),该路径仅作占位与选中态回读。
|
|
51
34
|
get { return new[] { new CodeEditor.Installation { Name = EditorName, Path = RealPath() } }; }
|
|
52
35
|
}
|
|
53
36
|
|
|
54
|
-
/// <summary
|
|
37
|
+
/// <summary>Use a real installation path for Unity's dropdown. @author ddj 2026-09-28</summary>
|
|
38
|
+
/// <returns>Unity executable or the legacy virtual identifier.</returns>
|
|
55
39
|
internal static string RealPath()
|
|
56
40
|
{
|
|
57
41
|
try
|
|
@@ -59,31 +43,36 @@ namespace Dsh.EditorIntegration
|
|
|
59
43
|
var path = EditorApplication.applicationPath;
|
|
60
44
|
return string.IsNullOrEmpty(path) ? VirtualPath : path;
|
|
61
45
|
}
|
|
62
|
-
catch (Exception)
|
|
63
|
-
{
|
|
64
|
-
return VirtualPath;
|
|
65
|
-
}
|
|
46
|
+
catch (Exception) { return VirtualPath; }
|
|
66
47
|
}
|
|
67
48
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
49
|
+
/// <summary>Unity callback; no project mutation is needed. @author ddj 2026-09-28</summary>
|
|
50
|
+
/// <param name="editorInstallationPath">Unity-selected installation.</param>
|
|
51
|
+
public void Initialize(string editorInstallationPath) { }
|
|
71
52
|
|
|
53
|
+
/// <summary>Select bridge metadata independently for each project. @author ddj 2026-09-28</summary>
|
|
72
54
|
public void OnGUI()
|
|
73
55
|
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
56
|
+
string key = ConfigKey();
|
|
57
|
+
string selected = EditorPrefs.GetString(key, string.Empty);
|
|
58
|
+
string value = EditorGUILayout.TextField("DSH 桥接配置", selected);
|
|
59
|
+
if (value != selected) EditorPrefs.SetString(key, value.Trim());
|
|
60
|
+
EditorGUILayout.HelpBox("留空时自动读取本项目 UserSettings/dsh-editor.ini;没有安装提示时使用 DSH_HOME 中的旧 shell 配置。填写绝对路径可覆盖自动选择。请求须收到 ACK 才确认成功。", MessageType.Info);
|
|
61
|
+
if (GUILayout.Button("测试:在 DSH 中打开当前项目")) BeginOpen(ProjectRoot(), 0, 0);
|
|
77
62
|
}
|
|
78
63
|
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
}
|
|
64
|
+
/// <summary>Unity callback; DSH does not generate project files. @author ddj 2026-09-28</summary>
|
|
65
|
+
public void SyncAll() { }
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
67
|
+
/// <summary>Unity callback; no generated solution needs synchronization. @author ddj 2026-09-28</summary>
|
|
68
|
+
/// <param name="addedFiles">Added assets.</param><param name="deletedFiles">Deleted assets.</param>
|
|
69
|
+
/// <param name="movedFiles">Moved assets.</param><param name="movedFromFiles">Previous paths.</param>
|
|
70
|
+
/// <param name="importedFiles">Imported assets.</param>
|
|
71
|
+
public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles) { }
|
|
86
72
|
|
|
73
|
+
/// <summary>Recognize the real or legacy virtual installation. @author ddj 2026-09-28</summary>
|
|
74
|
+
/// <param name="editorPath">Selected path.</param><param name="installation">Matching entry.</param>
|
|
75
|
+
/// <returns>Whether this editor owns the selection.</returns>
|
|
87
76
|
public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation)
|
|
88
77
|
{
|
|
89
78
|
if (string.Equals(editorPath, VirtualPath, StringComparison.OrdinalIgnoreCase)
|
|
@@ -96,25 +85,22 @@ namespace Dsh.EditorIntegration
|
|
|
96
85
|
return false;
|
|
97
86
|
}
|
|
98
87
|
|
|
88
|
+
/// <summary>Dispatch supported text assets; empty paths open the project. @author ddj 2026-09-28</summary>
|
|
89
|
+
/// <param name="path">Asset path or empty project request.</param><param name="line">One-based line.</param>
|
|
90
|
+
/// <param name="column">One-based column.</param><returns>Whether the asset belongs to this editor.</returns>
|
|
99
91
|
public bool OpenProject(string path, int line, int column)
|
|
100
92
|
{
|
|
101
|
-
// Open C# Project(Assets 菜单)传入空路径:视为打开 Unity 项目根(按文件夹规则路由)
|
|
102
93
|
if (string.IsNullOrEmpty(path))
|
|
103
94
|
{
|
|
104
95
|
BeginOpen(ProjectRoot(), line, column);
|
|
105
96
|
return true;
|
|
106
97
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
var absPath = ResolveAbsolute(path);
|
|
111
|
-
if (!IsSupportedFile(absPath)) return false;
|
|
112
|
-
|
|
113
|
-
BeginOpen(absPath, line, column);
|
|
98
|
+
string absolute = ResolveAbsolute(path);
|
|
99
|
+
if (!IsSupportedFile(absolute)) return false;
|
|
100
|
+
BeginOpen(absolute, line, column);
|
|
114
101
|
return true;
|
|
115
102
|
}
|
|
116
103
|
|
|
117
|
-
/// <summary>文本/代码类扩展名白名单:官方 DefaultExternalCodeEditor 支持集 + 常见文本/脚本类型。</summary>
|
|
118
104
|
private static readonly string[] SupportedExtensions =
|
|
119
105
|
{
|
|
120
106
|
"cs", "txt", "log", "json", "xml", "md", "yaml", "yml", "meta", "ini", "csv", "tsv",
|
|
@@ -124,190 +110,100 @@ namespace Dsh.EditorIntegration
|
|
|
124
110
|
"asmdef", "asmref", "uxml", "uss"
|
|
125
111
|
};
|
|
126
112
|
|
|
127
|
-
/// <summary>
|
|
128
|
-
///
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
var userExts = EditorSettings.projectGenerationUserExtensions;
|
|
141
|
-
if (userExts == null) return false;
|
|
142
|
-
foreach (var userExt in userExts)
|
|
113
|
+
/// <summary>Keep native prefab/scene handling and the existing text whitelist. @author ddj 2026-09-28</summary>
|
|
114
|
+
/// <param name="absolute">Absolute asset path.</param><returns>True for supported text assets.</returns>
|
|
115
|
+
private static bool IsSupportedFile(string absolute)
|
|
116
|
+
{
|
|
117
|
+
string extension = Path.GetExtension(absolute);
|
|
118
|
+
if (string.IsNullOrEmpty(extension)) return false;
|
|
119
|
+
extension = extension.Substring(1).ToLowerInvariant();
|
|
120
|
+
if (extension == "prefab" || extension == "unity") return false;
|
|
121
|
+
if (Array.IndexOf(SupportedExtensions, extension) >= 0) return true;
|
|
122
|
+
var custom = EditorSettings.projectGenerationUserExtensions;
|
|
123
|
+
if (custom == null) return false;
|
|
124
|
+
foreach (string value in custom)
|
|
143
125
|
{
|
|
144
|
-
if (string.Equals(
|
|
145
|
-
|
|
126
|
+
if (string.Equals(value, extension, StringComparison.OrdinalIgnoreCase)
|
|
127
|
+
|| string.Equals(value, "." + extension, StringComparison.OrdinalIgnoreCase)) return true;
|
|
146
128
|
}
|
|
147
129
|
return false;
|
|
148
130
|
}
|
|
149
131
|
|
|
150
|
-
/// <summary>
|
|
151
|
-
///
|
|
152
|
-
|
|
153
|
-
/// 后台线程只做网络 IO 与纯计算。
|
|
154
|
-
/// @author ddj 2026年09月08号
|
|
155
|
-
/// </summary>
|
|
156
|
-
private static void BeginOpen(string absPath, int line, int column)
|
|
132
|
+
/// <summary>Capture preferences on the main thread before starting disk IO. @author ddj 2026-09-28</summary>
|
|
133
|
+
/// <param name="path">Absolute target.</param><param name="line">One-based line.</param><param name="column">One-based column.</param>
|
|
134
|
+
private static void BeginOpen(string path, int line, int column)
|
|
157
135
|
{
|
|
158
|
-
string
|
|
159
|
-
var
|
|
160
|
-
{
|
|
161
|
-
try
|
|
162
|
-
{
|
|
163
|
-
if (HandoffToOpenPage(baseUrl, absPath, line, column)) return;
|
|
164
|
-
OpenUrlOnMain(BuildOpenUrl(baseUrl, absPath, line, column));
|
|
165
|
-
}
|
|
166
|
-
catch (Exception)
|
|
167
|
-
{
|
|
168
|
-
OpenUrlOnMain(BuildOpenUrl(baseUrl, absPath, line, column));
|
|
169
|
-
}
|
|
170
|
-
}) { IsBackground = true };
|
|
136
|
+
string config = EditorPrefs.GetString(ConfigKey(), string.Empty);
|
|
137
|
+
var worker = new DshOpenWorker(config, path, line, column, ProjectRoot(), LegacyConfig());
|
|
138
|
+
var thread = new Thread(worker.Run) { IsBackground = true };
|
|
171
139
|
thread.Start();
|
|
172
140
|
}
|
|
173
141
|
|
|
174
|
-
/// <summary
|
|
175
|
-
|
|
176
|
-
{
|
|
177
|
-
string resp = PostRpc(baseUrl, "edrv.external.handoff", HandoffArgs(absPath, line, column));
|
|
178
|
-
if (ExtractInt(resp, "clients") <= 0) return false;
|
|
179
|
-
Thread.Sleep(2000);
|
|
180
|
-
string token = ExtractString(resp, "token");
|
|
181
|
-
string state = PostRpc(baseUrl, "edrv.external.pendingState",
|
|
182
|
-
"{\"token\":" + JsonEscape(token) + ",\"take\":true}");
|
|
183
|
-
return ExtractBool(state, "delivered");
|
|
184
|
-
}
|
|
142
|
+
/// <summary>Derive a project-specific preference key. @author ddj 2026-09-28</summary>
|
|
143
|
+
/// <returns>Project-local editor preference key.</returns>
|
|
144
|
+
private static string ConfigKey() { return "DshEditor.Config." + ProjectRoot(); }
|
|
185
145
|
|
|
186
|
-
/// <summary
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (column > 0) json += ",\"column\":" + column;
|
|
192
|
-
return json + "}";
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/// <summary>POST /edrv/rpc;失败返回 null(调用方回退深链,不阻塞)。</summary>
|
|
196
|
-
private static string PostRpc(string baseUrl, string method, string argsJson)
|
|
146
|
+
/// <summary>Read the project's installed profile hint on the worker. @author ddj 2026-09-28</summary>
|
|
147
|
+
/// <param name="root">Project root captured on the main thread.</param><param name="fallback">Legacy shell INI.</param>
|
|
148
|
+
/// <returns>The hinted absolute INI, or legacy path only when the hint is absent.</returns>
|
|
149
|
+
/// <exception cref="InvalidDataException">The installed hint is malformed.</exception>
|
|
150
|
+
internal static string DefaultConfig(string root, string fallback)
|
|
197
151
|
{
|
|
152
|
+
string hint = Path.Combine(Path.Combine(root, "UserSettings"), "dsh-editor.ini");
|
|
198
153
|
try
|
|
199
154
|
{
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
155
|
+
if (new FileInfo(hint).Length > 65536) throw new InvalidDataException("DSH project bridge hint exceeds 64 KiB.");
|
|
156
|
+
foreach (string text in File.ReadAllLines(hint))
|
|
157
|
+
{
|
|
158
|
+
int equal = text.IndexOf('=');
|
|
159
|
+
if (equal < 0 || !string.Equals(text.Substring(0, equal).Trim(), "config", StringComparison.OrdinalIgnoreCase)) continue;
|
|
160
|
+
string value = text.Substring(equal + 1).Trim();
|
|
161
|
+
if (string.IsNullOrEmpty(value) || !Path.IsPathRooted(value))
|
|
162
|
+
throw new InvalidDataException("DSH project bridge hint requires an absolute config path.");
|
|
163
|
+
return Path.GetFullPath(value);
|
|
164
|
+
}
|
|
165
|
+
throw new InvalidDataException("DSH project bridge hint is missing config=. Reinstall the DSH Unity bridge or select an explicit configuration.");
|
|
211
166
|
}
|
|
212
|
-
catch (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
/// <summary>回退开浏览器需主线程:经 delayCall 切回。</summary>
|
|
216
|
-
private static void OpenUrlOnMain(string url)
|
|
217
|
-
{
|
|
218
|
-
EditorApplication.delayCall += () => Application.OpenURL(url);
|
|
167
|
+
catch (FileNotFoundException) { return fallback; }
|
|
168
|
+
catch (DirectoryNotFoundException) { return fallback; }
|
|
219
169
|
}
|
|
220
170
|
|
|
221
|
-
/// <summary
|
|
222
|
-
|
|
171
|
+
/// <summary>Honor custom DSH_HOME for legacy installations. @author ddj 2026-09-28</summary>
|
|
172
|
+
/// <returns>Legacy installed shell INI path, without filesystem IO.</returns>
|
|
173
|
+
private static string LegacyConfig()
|
|
223
174
|
{
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
return url;
|
|
175
|
+
string home = Environment.GetEnvironmentVariable("DSH_HOME");
|
|
176
|
+
if (string.IsNullOrEmpty(home)) home = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".dsh");
|
|
177
|
+
return Path.Combine(Path.Combine(Path.Combine(home, "dsh-vscode-mode"), "shell"), "dsh-open.ini");
|
|
228
178
|
}
|
|
229
179
|
|
|
230
|
-
/// <summary
|
|
180
|
+
/// <summary>Resolve asset paths from the Unity project root. @author ddj 2026-09-28</summary>
|
|
181
|
+
/// <param name="path">Asset path.</param><returns>Absolute path.</returns>
|
|
231
182
|
private static string ResolveAbsolute(string path)
|
|
232
183
|
{
|
|
233
|
-
return Path.IsPathRooted(path) ? path : Path.
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/// <summary>Unity 项目根(Assets 的父目录)。</summary>
|
|
237
|
-
private static string ProjectRoot()
|
|
238
|
-
{
|
|
239
|
-
return Directory.GetParent(Application.dataPath).FullName;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/// <summary>读取 EditorPrefs 的 DSH 服务地址(空回退默认)。</summary>
|
|
243
|
-
private static string BaseUrl()
|
|
244
|
-
{
|
|
245
|
-
var stored = EditorPrefs.GetString(BaseUrlPref, DefaultBaseUrl);
|
|
246
|
-
return string.IsNullOrEmpty(stored) ? DefaultBaseUrl : stored;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/// <summary>极简 JSON int 字段提取(缺失/失败 → -1)。起点 = 匹配串长度 key.Length+3("key":)。</summary>
|
|
250
|
-
private static int ExtractInt(string json, string key)
|
|
251
|
-
{
|
|
252
|
-
if (string.IsNullOrEmpty(json)) return -1;
|
|
253
|
-
int at = json.IndexOf("\"" + key + "\":", StringComparison.Ordinal);
|
|
254
|
-
if (at < 0) return -1;
|
|
255
|
-
at += key.Length + 3;
|
|
256
|
-
int end = at;
|
|
257
|
-
while (end < json.Length && (char.IsDigit(json[end]) || (end == at && json[end] == '-'))) end++;
|
|
258
|
-
int value;
|
|
259
|
-
return int.TryParse(json.Substring(at, end - at), out value) ? value : -1;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/// <summary>极简 JSON string 字段提取(缺失/失败 → null)。起点 = 匹配串长度 key.Length+4("key":")。</summary>
|
|
263
|
-
private static string ExtractString(string json, string key)
|
|
264
|
-
{
|
|
265
|
-
if (string.IsNullOrEmpty(json)) return null;
|
|
266
|
-
int at = json.IndexOf("\"" + key + "\":\"", StringComparison.Ordinal);
|
|
267
|
-
if (at < 0) return null;
|
|
268
|
-
at += key.Length + 4;
|
|
269
|
-
int end = json.IndexOf('"', at);
|
|
270
|
-
return end < 0 ? null : json.Substring(at, end - at);
|
|
184
|
+
return Path.GetFullPath(Path.IsPathRooted(path) ? path : Path.Combine(ProjectRoot(), path));
|
|
271
185
|
}
|
|
272
186
|
|
|
273
|
-
/// <summary
|
|
274
|
-
|
|
275
|
-
{
|
|
276
|
-
if (string.IsNullOrEmpty(json)) return false;
|
|
277
|
-
int at = json.IndexOf("\"" + key + "\":", StringComparison.Ordinal);
|
|
278
|
-
return at >= 0 && json.IndexOf("true", at, StringComparison.Ordinal) == at + key.Length + 3;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/// <summary>JSON 字符串转义(反斜杠 + 引号)。</summary>
|
|
282
|
-
private static string JsonEscape(string value)
|
|
283
|
-
{
|
|
284
|
-
return "\"" + (value ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
|
|
285
|
-
}
|
|
187
|
+
/// <summary>Read the current project root on the main thread. @author ddj 2026-09-28</summary>
|
|
188
|
+
/// <returns>Assets parent directory.</returns>
|
|
189
|
+
private static string ProjectRoot() { return Directory.GetParent(Application.dataPath).FullName; }
|
|
286
190
|
}
|
|
287
191
|
|
|
288
|
-
/// <summary>
|
|
289
|
-
/// 注册兜底:独立类的 InitializeOnLoadMethod——主类静态构造异常导致注册缺失时补注册。
|
|
290
|
-
/// @author ddj 2026-09-08
|
|
291
|
-
/// </summary>
|
|
292
192
|
internal static class DshCodeEditorBoot
|
|
293
193
|
{
|
|
194
|
+
/// <summary>Schedule fallback registration after Unity initializes. @author ddj 2026-09-28</summary>
|
|
294
195
|
[InitializeOnLoadMethod]
|
|
295
|
-
private static void EnsureRegistered()
|
|
196
|
+
private static void EnsureRegistered() { EditorApplication.delayCall += Register; }
|
|
197
|
+
|
|
198
|
+
/// <summary>Register only when another instance has not done so. @author ddj 2026-09-28</summary>
|
|
199
|
+
private static void Register()
|
|
296
200
|
{
|
|
297
|
-
|
|
201
|
+
try
|
|
298
202
|
{
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
CodeEditor.Register(new DshCodeEditor());
|
|
304
|
-
Debug.Log("[DshCodeEditor] 兜底注册完成(v" + DshCodeEditor.VersionText + ")");
|
|
305
|
-
}
|
|
306
|
-
catch (Exception e)
|
|
307
|
-
{
|
|
308
|
-
Debug.LogError("[DshCodeEditor] 兜底注册失败:" + e);
|
|
309
|
-
}
|
|
310
|
-
};
|
|
203
|
+
if (CodeEditor.Editor.GetCodeEditorForPath(DshCodeEditor.RealPath()) is DshCodeEditor) return;
|
|
204
|
+
CodeEditor.Register(new DshCodeEditor());
|
|
205
|
+
}
|
|
206
|
+
catch (Exception error) { Debug.LogError("[DshCodeEditor] 注册失败:" + error); }
|
|
311
207
|
}
|
|
312
208
|
}
|
|
313
209
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Background process adapter; no Unity preference or asset API is used on this thread.
|
|
2
|
+
// @author ddj 2026-09-28
|
|
3
|
+
using System;
|
|
4
|
+
using System.Collections.Generic;
|
|
5
|
+
using System.Diagnostics;
|
|
6
|
+
using System.IO;
|
|
7
|
+
using System.Text;
|
|
8
|
+
using UnityEditor;
|
|
9
|
+
using UnityEngine;
|
|
10
|
+
|
|
11
|
+
namespace Dsh.EditorIntegration
|
|
12
|
+
{
|
|
13
|
+
internal sealed class DshOpenWorker
|
|
14
|
+
{
|
|
15
|
+
private readonly string configPath;
|
|
16
|
+
private readonly string targetPath;
|
|
17
|
+
private readonly string projectRoot;
|
|
18
|
+
private readonly string legacyConfig;
|
|
19
|
+
private readonly int line;
|
|
20
|
+
private readonly int column;
|
|
21
|
+
|
|
22
|
+
/// <summary>Capture immutable request data on the main thread. @author ddj 2026-09-28</summary>
|
|
23
|
+
/// <param name="config">Selected INI path.</param><param name="path">Absolute asset or project.</param>
|
|
24
|
+
/// <param name="row">One-based line.</param><param name="col">One-based column.</param>
|
|
25
|
+
/// <param name="root">Project root.</param><param name="fallback">Legacy shell INI path.</param>
|
|
26
|
+
internal DshOpenWorker(string config, string path, int row, int col, string root, string fallback)
|
|
27
|
+
{
|
|
28
|
+
configPath = config;
|
|
29
|
+
targetPath = path;
|
|
30
|
+
projectRoot = root;
|
|
31
|
+
legacyConfig = fallback;
|
|
32
|
+
line = row;
|
|
33
|
+
column = col;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// <summary>Wait for the disk producer; failures are posted to the main thread. @author ddj 2026-09-28</summary>
|
|
37
|
+
internal void Run()
|
|
38
|
+
{
|
|
39
|
+
try { RunProducer(); }
|
|
40
|
+
catch (Exception error)
|
|
41
|
+
{
|
|
42
|
+
string message = error.Message;
|
|
43
|
+
EditorApplication.delayCall += delegate { UnityEngine.Debug.LogError("[DshCodeEditor] OPEN 未确认:" + message); };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// <summary>Read configuration on the worker and wait for a matching ACK. @author ddj 2026-09-28</summary>
|
|
48
|
+
private void RunProducer()
|
|
49
|
+
{
|
|
50
|
+
string selected = SelectConfig();
|
|
51
|
+
Dictionary<string, string> config = ReadIni(selected);
|
|
52
|
+
string node = FilePath(config, "node");
|
|
53
|
+
string helper = FilePath(config, "helper");
|
|
54
|
+
string mode;
|
|
55
|
+
if (!config.TryGetValue("nodeMode", out mode) || (mode != "node" && mode != "electron"))
|
|
56
|
+
throw new InvalidDataException("Missing nodeMode in DSH bridge configuration.");
|
|
57
|
+
var start = new ProcessStartInfo(node, Arguments(helper, selected));
|
|
58
|
+
start.UseShellExecute = false;
|
|
59
|
+
start.CreateNoWindow = true;
|
|
60
|
+
start.RedirectStandardOutput = true;
|
|
61
|
+
start.RedirectStandardError = true;
|
|
62
|
+
if (mode == "electron") start.EnvironmentVariables["ELECTRON_RUN_AS_NODE"] = "1";
|
|
63
|
+
else start.EnvironmentVariables.Remove("ELECTRON_RUN_AS_NODE");
|
|
64
|
+
WaitProducer(start);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// <summary>Drain diagnostic streams and dispatch only fixed wake messages. @author ddj 2026-09-28</summary>
|
|
68
|
+
/// <param name="start">Producer process configuration.</param>
|
|
69
|
+
private static void WaitProducer(ProcessStartInfo start)
|
|
70
|
+
{
|
|
71
|
+
using (var child = new Process())
|
|
72
|
+
{
|
|
73
|
+
child.StartInfo = start;
|
|
74
|
+
var errors = new StringBuilder();
|
|
75
|
+
DataReceivedEventHandler onError = delegate(object sender, DataReceivedEventArgs args)
|
|
76
|
+
{
|
|
77
|
+
if (args.Data != null) { lock (errors) { if (errors.Length < 8192) errors.AppendLine(args.Data); } }
|
|
78
|
+
};
|
|
79
|
+
child.ErrorDataReceived += onError;
|
|
80
|
+
child.Start();
|
|
81
|
+
child.BeginErrorReadLine();
|
|
82
|
+
string message;
|
|
83
|
+
while ((message = child.StandardOutput.ReadLine()) != null) WakeMessage(message);
|
|
84
|
+
child.WaitForExit();
|
|
85
|
+
child.ErrorDataReceived -= onError;
|
|
86
|
+
if (child.ExitCode != 0) throw new IOException(errors.Length == 0 ? "Missing successful OPEN ACK." : errors.ToString());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// <summary>Perform UI activation exclusively through Unity's main-thread delayCall. @author ddj 2026-09-28</summary>
|
|
91
|
+
/// <param name="message">Producer control line, never an asset path.</param>
|
|
92
|
+
private static void WakeMessage(string message)
|
|
93
|
+
{
|
|
94
|
+
if (!message.StartsWith("DSH_WAKE:", StringComparison.Ordinal)) return;
|
|
95
|
+
string target = message.Substring(9);
|
|
96
|
+
Uri uri;
|
|
97
|
+
if (target != "dsh://open" && (!Uri.TryCreate(target, UriKind.Absolute, out uri)
|
|
98
|
+
|| (uri.Scheme != "http" && uri.Scheme != "https") || !string.IsNullOrEmpty(uri.UserInfo)
|
|
99
|
+
|| !string.IsNullOrEmpty(uri.Query) || !string.IsNullOrEmpty(uri.Fragment)))
|
|
100
|
+
throw new InvalidDataException("Invalid bridge wake target.");
|
|
101
|
+
EditorApplication.delayCall += delegate { Application.OpenURL(target); };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// <summary>Prefer the captured explicit override without reading a project hint. @author ddj 2026-09-28</summary>
|
|
105
|
+
/// <returns>Selected bridge INI path; missing or malformed hints are handled by the caller.</returns>
|
|
106
|
+
internal string SelectConfig()
|
|
107
|
+
{
|
|
108
|
+
return string.IsNullOrEmpty(configPath) ? DshCodeEditor.DefaultConfig(projectRoot, legacyConfig) : configPath;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// <summary>Build helper arguments without routing files through a URL. @author ddj 2026-09-28</summary>
|
|
112
|
+
/// <param name="helper">Absolute JS producer.</param><param name="selected">Resolved INI.</param><returns>Process command line.</returns>
|
|
113
|
+
private string Arguments(string helper, string selected)
|
|
114
|
+
{
|
|
115
|
+
string args = Quote(helper) + " --config " + Quote(selected) + " --no-wake";
|
|
116
|
+
if (line > 0) args += " --line " + line;
|
|
117
|
+
if (column > 0) args += " --column " + column;
|
|
118
|
+
return args + " -- " + Quote(targetPath);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// <summary>Read selected bridge metadata without guessing a profile. @author ddj 2026-09-28</summary>
|
|
122
|
+
/// <param name="selected">Resolved INI.</param><returns>Case-insensitive INI values.</returns>
|
|
123
|
+
private static Dictionary<string, string> ReadIni(string selected)
|
|
124
|
+
{
|
|
125
|
+
if (!Path.IsPathRooted(selected) || !File.Exists(selected))
|
|
126
|
+
throw new FileNotFoundException("Select an absolute, installed DSH bridge configuration in External Tools.");
|
|
127
|
+
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
128
|
+
foreach (string text in File.ReadAllLines(selected, Encoding.UTF8))
|
|
129
|
+
{
|
|
130
|
+
int equal = text.IndexOf('=');
|
|
131
|
+
if (equal > 0) values[text.Substring(0, equal).Trim()] = text.Substring(equal + 1).Trim();
|
|
132
|
+
}
|
|
133
|
+
return values;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// <summary>Require the stored interpreter and helper to exist. @author ddj 2026-09-28</summary>
|
|
137
|
+
/// <param name="config">INI values.</param><param name="key">Metadata key.</param><returns>Absolute file path.</returns>
|
|
138
|
+
private static string FilePath(Dictionary<string, string> config, string key)
|
|
139
|
+
{
|
|
140
|
+
string path;
|
|
141
|
+
if (!config.TryGetValue(key, out path) || !Path.IsPathRooted(path) || !File.Exists(path))
|
|
142
|
+
throw new FileNotFoundException("Missing absolute " + key + " in DSH bridge configuration.");
|
|
143
|
+
if (key == "node" && Path.DirectorySeparatorChar == '\\'
|
|
144
|
+
&& !string.Equals(Path.GetExtension(path), ".exe", StringComparison.OrdinalIgnoreCase))
|
|
145
|
+
throw new InvalidDataException("A stored Node or Electron executable is required.");
|
|
146
|
+
return path;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// <summary>Quote one argument for .NET ProcessStartInfo. @author ddj 2026-09-28</summary>
|
|
150
|
+
/// <param name="value">Literal argument.</param><returns>Escaped argument.</returns>
|
|
151
|
+
private static string Quote(string value)
|
|
152
|
+
{
|
|
153
|
+
var result = new StringBuilder("\"");
|
|
154
|
+
int slashes = 0;
|
|
155
|
+
foreach (char ch in value)
|
|
156
|
+
{
|
|
157
|
+
if (ch == '\\') { slashes++; continue; }
|
|
158
|
+
result.Append('\\', ch == '"' ? slashes * 2 + 1 : slashes);
|
|
159
|
+
result.Append(ch);
|
|
160
|
+
slashes = 0;
|
|
161
|
+
}
|
|
162
|
+
result.Append('\\', slashes * 2);
|
|
163
|
+
return result.Append('"').ToString();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -1,35 +1,26 @@
|
|
|
1
|
-
# com.dsh.editor — DSH
|
|
1
|
+
# com.dsh.editor — DSH 文件编辑
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
双击脚本 / Console 报错跳转时,优先把打开请求**移交已打开的 DSH 页面**就地执行(不重复开新页);
|
|
5
|
-
无活跃页面时回退经默认浏览器深链打开对应文件并定位行列。
|
|
3
|
+
Unity 外部脚本编辑器通过当前用户私有的 OPEN REQUEST/ACK 文件队列打开脚本或项目。收到 profile 与 requestId 匹配的成功 ACK 才确认完成;超时、拒绝或无效 ACK 会在 Unity Console 报告错误。
|
|
6
4
|
|
|
7
|
-
##
|
|
5
|
+
## 安装
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
2. 在 Unity 集成区添加你的 Unity 项目根(包含 `Assets/` 与 `ProjectSettings/` 的目录)。
|
|
11
|
-
3. 点「安装」—— 包会复制为 `<项目>/Packages/com.dsh.editor`(Unity 内嵌包,自动发现)。
|
|
12
|
-
4. 切回 Unity 窗口(或下次启动 Unity)自动生效。
|
|
7
|
+
在 DSH 设置 → VSCodeMode → 系统集成中添加 Unity 项目并安装/更新内嵌包,或在 Unity Package Manager 中选择本包。切回 Unity 后,在 Preferences → External Tools → External Script Editor 中选择 **DSH 文件编辑**。
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
DSH 安装 Unity 集成时会为当前 profile 准备独立的 `<profile>/dsh-vscode-mode/bridge`,并在项目的 `UserSettings/dsh-editor.ini` 写入 `config=<绝对桥接INI路径>`;无需先注册资源管理器菜单,也不会覆盖其他 profile 的菜单配置。
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
**DSH 桥接配置**字段为每个 Unity 项目单独保存。填写绝对路径时使用该显式覆盖;留空时,后台先读取本项目的安装提示,只有提示文件不存在才回退 `$DSH_HOME/dsh-vscode-mode/shell/dsh-open.ini`。未设 `DSH_HOME` 时旧目录以用户目录中的 `.dsh` 为根。提示损坏、相对路径或目标配置不可读会通过 Unity Console 报告错误,不会猜测端口或切换 profile。清空字段可恢复自动选择。
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
选择插件目录下的 `unity/com.dsh.editor`(含本 README 的目录)。
|
|
13
|
+
## 配置契约
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
INI 保留 `base`,并包含 `mode=desktop|web`、绝对 `profile`、绝对 `inbox`、绝对 `node` 可执行文件、`nodeMode=node|electron` 和绝对 `helper`(安装的 `dsh-open.mjs`)。配置由 DSH 生成,Unity 不推测端口或 profile,也不创建任何队列目录。
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
2. 展开 DSH 文件编辑选项,确认「DSH 服务地址」(默认 `http://127.0.0.1:3080`,需与 DSH Web GUI 地址一致)。
|
|
25
|
-
3. 双击任意脚本即可打开(有已打开 DSH 页面时直接在其页面内打开,不再新开标签);Console 报错双击会带上行列。
|
|
17
|
+
桌面模式仅唤起官方 `dsh://open`;Web 模式仅唤起所选 `base`。文件和行列只进入队列,不附加到 URL。Electron 解释器使用 `ELECTRON_RUN_AS_NODE=1`。producer 必须与 INI 一同安装,不能只复制 Unity 包后填写一个服务地址。
|
|
26
18
|
|
|
27
|
-
##
|
|
19
|
+
## 使用与边界
|
|
28
20
|
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
- 卸载:删除 `<项目>/Packages/com.dsh.editor` 目录,并在 External Script Editor 换回其他编辑器。
|
|
21
|
+
- 双击支持的文本/代码文件,或从 Console 跳转行列。原有文本扩展名白名单及自定义文本扩展保留;prefab 和 scene 始终交回 Unity。
|
|
22
|
+
- “Open C# Project” 的空路径打开 Unity 项目根;测试按钮也打开项目根。
|
|
23
|
+
- 偏好读取在主线程;配置/队列 IO 与等待在后台 producer 中。界面激活通过 `EditorApplication.delayCall` 执行,不在后台调用 Unity UI。
|
|
24
|
+
- 队列要求 Windows 当前用户独占 ACL,或 POSIX 目录 0700、文件 0600,并拒绝链接/重解析逃逸。目录缺失或不安全时失败,不建立宽松回退目录。
|
|
25
|
+
- 请求最多 20 个绝对路径,每个至多 8192 字符,JSON 至多 64 KiB,有效期及 ACK 等待上限 60 秒。不使用 cookies 或通用 RPC。
|
|
26
|
+
- 卸载时删除内嵌包并选择其他外部编辑器。个人配置保存在项目专属 EditorPrefs 键中,不修改项目资源。
|