From 02b0aca891f994bb172fc9c4b13f063cf8c48337 Mon Sep 17 00:00:00 2001 From: cruldra Date: Wed, 24 Jun 2026 05:29:19 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20PyCharm=20=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=91=BD=E4=BB=A4=E5=8F=AF=E9=85=8D=E7=BD=AE=EF=BC=88?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20~/=E7=BB=9D=E5=AF=B9=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E6=8A=BD=20expandTilde=20=E5=A4=8D=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GUI 启动的 VS Code 扩展进程 PATH 常不含 JetBrains 启动器,裸 pycharm 易 ENOENT。 新增设置 pycharmCommand(默认 pycharm),贯穿 store/settings handler/messages/ KanbanPanel 与 webview,「钩子」分组在选 PyCharm 时显示「PyCharm 启动命令」输入框。 handleOpenWorktree 用该命令并经 expandTilde 展开 ~。expandTilde 从 resolveWorktreePath 抽出共用。 Co-Authored-By: Claude Opus 4.7 (1M context) --- vscode/src/git/worktree.ts | 15 +++++--- vscode/src/panel/KanbanPanel.ts | 2 ++ vscode/src/panel/handlers/settings.ts | 7 ++++ vscode/src/panel/handlers/worktree.ts | 11 +++--- vscode/src/panel/messages.ts | 2 ++ vscode/src/settings/store.ts | 14 ++++++++ vscode/webview-ui/src/App.tsx | 1 + .../src/components/SettingsModal.tsx | 35 +++++++++++++++++++ vscode/webview-ui/src/hooks/useIssues.ts | 4 +++ vscode/webview-ui/src/lib/messages.ts | 2 ++ 10 files changed, 84 insertions(+), 9 deletions(-) diff --git a/vscode/src/git/worktree.ts b/vscode/src/git/worktree.ts index 76b04e0..5445079 100644 --- a/vscode/src/git/worktree.ts +++ b/vscode/src/git/worktree.ts @@ -10,16 +10,21 @@ import { execFile } from 'node:child_process' import * as os from 'node:os' import * as path from 'node:path' +/** + * 把开头的 `~` 展开为用户 home 目录。execFile 不走 shell,传给它的命令/路径 + * 里的 `~` 不会被展开,需要我们自己处理。 + */ +export function expandTilde(p: string): string { + return p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p +} + /** * 把存储的 worktreePath 解析成可用的绝对路径。三态合一: - * 开头 `~` → 展开为 home(git/execFile 不走 shell,自己不展开 `~`, - * 存量工单里可能存了带 `~` 的字面量);已是绝对 → 原样; + * 开头 `~` → 展开为 home(存量工单里可能存了带 `~` 的字面量);已是绝对 → 原样; * 相对 → 拼到 workspaceRoot 下(旧的 .claude/worktrees/ 方案)。 */ export function resolveWorktreePath(stored: string, workspaceRoot: string): string { - let p = stored - if (p.startsWith('~')) - p = path.join(os.homedir(), p.slice(1)) + const p = expandTilde(stored) return path.isAbsolute(p) ? p : path.join(workspaceRoot, p) } diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index 8e7de09..8dcba2e 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -842,6 +842,7 @@ export class KanbanWebviewPanel { autoBuildBranch: s.autoBuildBranch, worktreeDirectory: s.worktreeDirectory, worktreeOpenWith: s.worktreeOpenWith, + pycharmCommand: s.pycharmCommand, worktreePostCreateScript: s.worktreePostCreateScript, worktreePreRemoveScript: s.worktreePreRemoveScript, implTabPreCreateScript: s.implTabPreCreateScript, @@ -903,6 +904,7 @@ export class KanbanWebviewPanel { autoBuildBranch: s.autoBuildBranch, worktreeDirectory: s.worktreeDirectory, worktreeOpenWith: s.worktreeOpenWith, + pycharmCommand: s.pycharmCommand, worktreePostCreateScript: s.worktreePostCreateScript, worktreePreRemoveScript: s.worktreePreRemoveScript, implTabPreCreateScript: s.implTabPreCreateScript, diff --git a/vscode/src/panel/handlers/settings.ts b/vscode/src/panel/handlers/settings.ts index a6ff20e..f64987e 100644 --- a/vscode/src/panel/handlers/settings.ts +++ b/vscode/src/panel/handlers/settings.ts @@ -27,6 +27,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { autoBuildBranch: string worktreeDirectory: string worktreeOpenWith: 'vscode' | 'pycharm' + pycharmCommand: string worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string @@ -48,6 +49,9 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { // worktree dir template: '' = "use default", so don't preserve blanks // specially — getSettings falls back to the default at read time. const trimmedWorktreeDir = payload.worktreeDirectory.trim() + // pycharm launcher command: '' = "use default `pycharm`", getSettings falls + // back at read time, so just strip whitespace here. + const trimmedPycharmCmd = payload.pycharmCommand.trim() // Hook script paths: keep '' meaningful (= "use default // .spx/*.sh"). Just strip whitespace. const trimmedPostCreate = payload.worktreePostCreateScript.trim() @@ -78,6 +82,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { autoBuildBranch: trimmedAutoBuildBranch, worktreeDirectory: trimmedWorktreeDir || prev.worktreeDirectory, worktreeOpenWith: payload.worktreeOpenWith || prev.worktreeOpenWith, + pycharmCommand: trimmedPycharmCmd || prev.pycharmCommand, worktreePostCreateScript: trimmedPostCreate, worktreePreRemoveScript: trimmedPreRemove, implTabPreCreateScript: trimmedImplPre, @@ -98,6 +103,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { autoBuildBranch: trimmedAutoBuildBranch, worktreeDirectory: trimmedWorktreeDir, worktreeOpenWith: payload.worktreeOpenWith, + pycharmCommand: trimmedPycharmCmd, worktreePostCreateScript: trimmedPostCreate, worktreePreRemoveScript: trimmedPreRemove, implTabPreCreateScript: trimmedImplPre, @@ -179,6 +185,7 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom autoBuildBranch: s.autoBuildBranch, worktreeDirectory: s.worktreeDirectory, worktreeOpenWith: s.worktreeOpenWith, + pycharmCommand: s.pycharmCommand, worktreePostCreateScript: s.worktreePostCreateScript, worktreePreRemoveScript: s.worktreePreRemoveScript, implTabPreCreateScript: s.implTabPreCreateScript, diff --git a/vscode/src/panel/handlers/worktree.ts b/vscode/src/panel/handlers/worktree.ts index a559009..099fd9c 100644 --- a/vscode/src/panel/handlers/worktree.ts +++ b/vscode/src/panel/handlers/worktree.ts @@ -4,7 +4,7 @@ import { execFile } from 'node:child_process' import { commands, Uri, window, workspace } from 'vscode' import { getToken } from '../../auth/secrets' import { detectRepo } from '../../git/remote' -import { resolveWorktreePath } from '../../git/worktree' +import { expandTilde, resolveWorktreePath } from '../../git/worktree' import { runImplTabPostCloseHook, runImplTabPreCreateHook, @@ -32,13 +32,16 @@ export async function handleOpenWorktree(panel: KanbanWebviewPanel, relPath: str // PyCharm 分支用 JetBrains 启动器打开目录。错误在回调里自行处理,不要落进 // 下面 openFolder 的 catch。 - const { worktreeOpenWith } = getSettings(panel.context) + const { worktreeOpenWith, pycharmCommand } = getSettings(panel.context) if (worktreeOpenWith === 'pycharm') { - execFile('pycharm', [abs], (err) => { + // GUI 启动的 VS Code 进程 PATH 常不含 JetBrains 启动器,故命令可配置; + // 允许填 `~`/绝对路径,execFile 不走 shell,`~` 需自己展开。 + const cmd = expandTilde(pycharmCommand) + execFile(cmd, [abs], (err) => { if (err) { const code = (err as NodeJS.ErrnoException).code ?? 'unknown' const message = code === 'ENOENT' - ? '找不到 pycharm 命令,请在 JetBrains Toolbox 里启用命令行启动器或把 pycharm 加入 PATH' + ? `找不到 ${cmd} 命令,请在「PyCharm 启动命令」里填启动器的完整路径,或确保它在 PATH 中` : err.message void window.showErrorMessage(`用 PyCharm 打开失败: ${message}`) } diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index 46bbb81..eb050f7 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -63,6 +63,7 @@ export type ExtensionToWebview autoBuildBranch: string worktreeDirectory: string worktreeOpenWith: 'vscode' | 'pycharm' + pycharmCommand: string worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string @@ -119,6 +120,7 @@ export type WebviewToExtension autoBuildBranch: string worktreeDirectory: string worktreeOpenWith: 'vscode' | 'pycharm' + pycharmCommand: string worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string diff --git a/vscode/src/settings/store.ts b/vscode/src/settings/store.ts index acff90b..d852937 100644 --- a/vscode/src/settings/store.ts +++ b/vscode/src/settings/store.ts @@ -100,6 +100,13 @@ export interface Settings { * `'vscode'` (default) opens a new VS Code window. */ worktreeOpenWith: 'vscode' | 'pycharm' + /** + * Command used to launch PyCharm when `worktreeOpenWith === 'pycharm'`. + * A bare name relies on PATH, which a GUI-launched VS Code often lacks, so + * an absolute path (or a `~`-prefixed one) is the reliable choice. Empty + * means "use the default `pycharm`". + */ + pycharmCommand: string /** * Path to a user-provided shell script that runs *after* the extension * creates a worktree via `git worktree add`. Empty string means use the @@ -173,6 +180,7 @@ function defaults(ctx: ExtensionContext): Settings { autoBuildBranch: '', worktreeDirectory: '~/Sources/worktree/$project_name/$feature_name', worktreeOpenWith: 'vscode', + pycharmCommand: 'pycharm', worktreePostCreateScript: '', worktreePreRemoveScript: '', implTabPreCreateScript: '', @@ -226,6 +234,11 @@ export function getSettings(ctx: ExtensionContext): Settings { const worktreeOpenWith = stored.worktreeOpenWith === 'pycharm' || stored.worktreeOpenWith === 'vscode' ? stored.worktreeOpenWith : base.worktreeOpenWith + // pycharm launcher command: blank means "use the default `pycharm`", so fall + // back to base rather than preserving an empty string. + const pycharmCommand = typeof stored.pycharmCommand === 'string' && stored.pycharmCommand.length > 0 + ? stored.pycharmCommand + : base.pycharmCommand // worktree hook script paths: '' is meaningful (= "use default // .spx/worktree-*.sh"), so don't coerce. Non-string legacy values fall // back to ''. The hook runner resolves '' to the default at execution @@ -266,6 +279,7 @@ export function getSettings(ctx: ExtensionContext): Settings { autoBuildBranch, worktreeDirectory, worktreeOpenWith, + pycharmCommand, worktreePostCreateScript, worktreePreRemoveScript, implTabPreCreateScript, diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index 8c5ac3a..4f6f5fd 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -447,6 +447,7 @@ export function App() { initialAutoBuildBranch={settings?.autoBuildBranch ?? ''} initialWorktreeDirectory={settings?.worktreeDirectory ?? ''} initialWorktreeOpenWith={settings?.worktreeOpenWith ?? 'vscode'} + initialPycharmCommand={settings?.pycharmCommand ?? 'pycharm'} initialWorktreePostCreateScript={settings?.worktreePostCreateScript ?? ''} initialWorktreePreRemoveScript={settings?.worktreePreRemoveScript ?? ''} initialImplTabPreCreateScript={settings?.implTabPreCreateScript ?? ''} diff --git a/vscode/webview-ui/src/components/SettingsModal.tsx b/vscode/webview-ui/src/components/SettingsModal.tsx index 4455575..1486d3e 100644 --- a/vscode/webview-ui/src/components/SettingsModal.tsx +++ b/vscode/webview-ui/src/components/SettingsModal.tsx @@ -25,6 +25,7 @@ interface SubmitValues { autoBuildBranch: string worktreeDirectory: string worktreeOpenWith: 'vscode' | 'pycharm' + pycharmCommand: string worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string @@ -63,6 +64,7 @@ export interface SettingsModalProps { initialAutoBuildBranch: string initialWorktreeDirectory: string initialWorktreeOpenWith: 'vscode' | 'pycharm' + initialPycharmCommand: string initialWorktreePostCreateScript: string initialWorktreePreRemoveScript: string initialImplTabPreCreateScript: string @@ -134,6 +136,7 @@ export function SettingsModal({ initialAutoBuildBranch, initialWorktreeDirectory, initialWorktreeOpenWith, + initialPycharmCommand, initialWorktreePostCreateScript, initialWorktreePreRemoveScript, initialImplTabPreCreateScript, @@ -159,6 +162,7 @@ export function SettingsModal({ const [autoBuildBranch, setAutoBuildBranch] = useState(initialAutoBuildBranch) const [worktreeDirectory, setWorktreeDirectory] = useState(initialWorktreeDirectory) const [worktreeOpenWith, setWorktreeOpenWith] = useState(initialWorktreeOpenWith) + const [pycharmCommand, setPycharmCommand] = useState(initialPycharmCommand) const [worktreePostCreateScript, setWorktreePostCreateScript] = useState(initialWorktreePostCreateScript) const [worktreePreRemoveScript, setWorktreePreRemoveScript] = useState(initialWorktreePreRemoveScript) const [implTabPreCreateScript, setImplTabPreCreateScript] = useState(initialImplTabPreCreateScript) @@ -263,6 +267,8 @@ export function SettingsModal({ // worktree 目录模板:trim 后留空 = 用默认模板(getSettings 读时回退) worktreeDirectory: worktreeDirectory.trim(), worktreeOpenWith, + // pycharm 启动命令:trim 后留空 = 用默认 `pycharm`(getSettings 读时回退) + pycharmCommand: pycharmCommand.trim(), // 钩子路径保持原样 trim(前后空格无意义;空串 = 用默认 .spx/*.sh) worktreePostCreateScript: worktreePostCreateScript.trim(), worktreePreRemoveScript: worktreePreRemoveScript.trim(), @@ -669,6 +675,35 @@ export function SettingsModal({ + {worktreeOpenWith === 'pycharm' && ( + + 可填绝对路径或 + {' '} + ~ + {' '} + 开头路径(如 + {' '} + ~/.local/share/JetBrains/Toolbox/scripts/pycharm + );GUI 启动的 VS Code 可能找不到裸 + {' '} + pycharm + ,填全路径最稳。 + + )} + > + setPycharmCommand(e.target.value)} + placeholder="pycharm" + className={inputClass} + /> + + )} +