Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
run: npm ci

- name: Build and Publish Tauri Release
uses: tauri-apps/tauri-action@v2
uses: tauri-apps/tauri-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# To enable code signing & notarization, configure the secrets in GitHub and uncomment:
Expand Down
46 changes: 37 additions & 9 deletions crates/codelaunch-core/src/ide/terminal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,13 @@ fn collect_grouped_terminals(
}

fn resolve_folder_path(folder: &Folder) -> String {
let path = if folder.path.is_absolute() {
folder.path.clone()
} else {
std::env::current_dir()
.unwrap_or_default()
.join(&folder.path)
};
let folder_str = folder.path.to_string_lossy();
if folder.path.is_absolute() || folder_str.starts_with(r"\\") || folder_str.starts_with("//") {
return folder_str.into_owned();
}
let path = std::env::current_dir()
.unwrap_or_default()
.join(&folder.path);
path.to_string_lossy().into_owned()
}

Expand Down Expand Up @@ -678,7 +678,11 @@ fn generate_windows_terminal_script(term: &Terminal, folder: &Folder) -> String
let mut script = String::new();
script.push_str("@echo off\n");
script.push_str(&format!("title {}\n", term.label));
script.push_str(&format!("cd /d \"{}\"\n", folder_path));
if folder_path.starts_with(r"\\") {
script.push_str(&format!("pushd \"{}\"\n", folder_path));
} else {
script.push_str(&format!("cd /d \"{}\"\n", folder_path));
}

match (&term.command, term.keep_alive) {
(Some(cmd), true) => {
Expand Down Expand Up @@ -708,7 +712,11 @@ fn generate_windows_master_script(
if grouped_scripts.is_empty() {
if let Some(folder) = project.folders.first() {
let path = resolve_folder_path(folder);
script.push_str(&format!("start \"\" cmd /k \"cd /d \"{}\"\"\n", path));
if path.starts_with(r"\\") {
script.push_str(&format!("start \"\" cmd /k \"pushd \"{}\"\"\n", path));
} else {
script.push_str(&format!("start \"\" cmd /k \"cd /d \"{}\"\"\n", path));
}
}
} else {
script.push_str("where wt.exe >nul 2>&1\n");
Expand Down Expand Up @@ -940,6 +948,26 @@ mod tests {
assert!(term2_content.contains("exit"));
}

#[test]
fn render_for_windows_uses_pushd_for_unc_paths() {
let tmp = tempfile::tempdir().unwrap();
let mut project = Project::new("WSL Project", IdeKind::Terminal);
project.terminals_enabled = true;
let folder = Folder::new("backend", r"\\wsl.localhost\Ubuntu\home\tiyo\TEAEdu-back");
let folder_id = folder.id;
project.folders.push(folder);
let mut group = TerminalGroup::new("Main", 0);
let mut term = Terminal::new("Server", folder_id, 0);
term.command = Some("npm run dev".into());
term.keep_alive = true;
group.terminals.push(term);
project.terminal_groups.push(group);

let rendered = render_for_os(&project, tmp.path(), TargetOs::Windows).unwrap();
let term_content = fs::read_to_string(&rendered.generated_files[0]).unwrap();
assert!(term_content.contains(r#"pushd "\\wsl.localhost\Ubuntu\home\tiyo\TEAEdu-back""#));
}

#[test]
fn preview_contains_all_supported_os() {
let project = create_test_project();
Expand Down
94 changes: 81 additions & 13 deletions crates/codelaunch-core/src/ide/vscode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ impl IdeAdapter for VsCodeAdapter {

fn launch_command(&self, workspace: &RenderedWorkspace) -> Command {
let binary = resolve_cli_path(&self.cli_binary);
#[cfg(target_os = "windows")]
{
let binary_str = binary.to_string_lossy().to_lowercase();
if !binary_str.ends_with(".exe") {
let mut cmd = Command::new("cmd");
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
cmd.arg("/c")
.arg(&binary)
.arg("--new-window")
.arg(&workspace.entry_path);
return cmd;
}
}
let mut cmd = Command::new(binary);
cmd.arg("--new-window").arg(&workspace.entry_path);
cmd
Expand All @@ -85,9 +99,27 @@ pub fn resolve_cli_path(binary: &str) -> PathBuf {
// 1. Search in PATH
if let Some(path_var) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join(binary);
if candidate.is_file() {
return candidate;
#[cfg(target_os = "windows")]
{
// On Windows, do not match extensionless scripts (e.g. `bin/code` shell script).
// Search for executable extensions: .exe, .cmd, .bat
for ext in &[".exe", ".cmd", ".bat"] {
let candidate = if binary.to_lowercase().ends_with(ext) {
dir.join(binary)
} else {
dir.join(format!("{binary}{ext}"))
};
if candidate.is_file() {
return candidate;
}
}
}
#[cfg(not(target_os = "windows"))]
{
let candidate = dir.join(binary);
if candidate.is_file() {
return candidate;
}
}
}
}
Expand All @@ -104,8 +136,12 @@ pub fn resolve_cli_path(binary: &str) -> PathBuf {
#[cfg(target_os = "macos")]
"/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code",
#[cfg(target_os = "windows")]
"C:\\Program Files\\Microsoft VS Code\\Code.exe",
#[cfg(target_os = "windows")]
"C:\\Program Files\\Microsoft VS Code\\bin\\code.cmd",
#[cfg(target_os = "windows")]
"C:\\Program Files (x86)\\Microsoft VS Code\\Code.exe",
#[cfg(target_os = "windows")]
"C:\\Program Files (x86)\\Microsoft VS Code\\bin\\code.cmd",
#[cfg(target_os = "linux")]
"/usr/bin/code",
Expand All @@ -124,6 +160,8 @@ pub fn resolve_cli_path(binary: &str) -> PathBuf {
#[cfg(target_os = "macos")]
"/Applications/Cursor.app/Contents/MacOS/Cursor",
#[cfg(target_os = "windows")]
"C:\\Program Files\\Cursor\\Cursor.exe",
#[cfg(target_os = "windows")]
"C:\\Program Files\\Cursor\\bin\\cursor.cmd",
#[cfg(target_os = "windows")]
"C:\\Program Files\\Cursor\\resources\\app\\bin\\cursor.cmd",
Expand All @@ -144,6 +182,8 @@ pub fn resolve_cli_path(binary: &str) -> PathBuf {
#[cfg(target_os = "macos")]
"/Applications/VSCodium - Insiders.app/Contents/Resources/app/bin/codium",
#[cfg(target_os = "windows")]
"C:\\Program Files\\VSCodium\\VSCodium.exe",
#[cfg(target_os = "windows")]
"C:\\Program Files\\VSCodium\\bin\\codium.cmd",
#[cfg(target_os = "linux")]
"/usr/bin/codium",
Expand All @@ -162,6 +202,8 @@ pub fn resolve_cli_path(binary: &str) -> PathBuf {
#[cfg(target_os = "macos")]
"/Applications/Windsurf.app/Contents/MacOS/Windsurf",
#[cfg(target_os = "windows")]
"C:\\Program Files\\Windsurf\\Windsurf.exe",
#[cfg(target_os = "windows")]
"C:\\Program Files\\Windsurf\\bin\\windsurf.cmd",
#[cfg(target_os = "linux")]
"/usr/bin/windsurf",
Expand Down Expand Up @@ -221,13 +263,23 @@ pub fn resolve_cli_path(binary: &str) -> PathBuf {
{
let local_data = dirs.data_local_dir();
let user_candidates: Vec<PathBuf> = match binary {
"code" => vec![local_data.join("Programs/Microsoft VS Code/bin/code.cmd")],
"code" => vec![
local_data.join("Programs/Microsoft VS Code/Code.exe"),
local_data.join("Programs/Microsoft VS Code/bin/code.cmd"),
],
"cursor" => vec![
local_data.join("Programs/cursor/Cursor.exe"),
local_data.join("Programs/cursor/bin/cursor.cmd"),
local_data.join("Programs/cursor/resources/app/bin/cursor.cmd"),
],
"codium" => vec![local_data.join("Programs/VSCodium/bin/codium.cmd")],
"windsurf" => vec![local_data.join("Programs/windsurf/bin/windsurf.cmd")],
"codium" => vec![
local_data.join("Programs/VSCodium/VSCodium.exe"),
local_data.join("Programs/VSCodium/bin/codium.cmd"),
],
"windsurf" => vec![
local_data.join("Programs/windsurf/Windsurf.exe"),
local_data.join("Programs/windsurf/bin/windsurf.cmd"),
],
_ => vec![],
};
for candidate in user_candidates {
Expand Down Expand Up @@ -284,13 +336,13 @@ pub fn build_workspace_document(project: &Project) -> Result<VsCodeWorkspaceFile
fn resolve_folder_path(folder: &crate::model::Folder) -> String {
// The generated .code-workspace lives in CodeLaunch's own directory, not next to
// the user's repositories, so folder paths must be absolute rather than relative.
let path: PathBuf = if folder.path.is_absolute() {
folder.path.clone()
} else {
std::env::current_dir()
.unwrap_or_default()
.join(&folder.path)
};
let folder_str = folder.path.to_string_lossy();
if folder.path.is_absolute() || folder_str.starts_with(r"\\") || folder_str.starts_with("//") {
return folder_str.into_owned();
}
let path = std::env::current_dir()
.unwrap_or_default()
.join(&folder.path);
path.to_string_lossy().into_owned()
}

Expand Down Expand Up @@ -386,4 +438,20 @@ mod tests {
.ends_with("renamed-project.code-workspace"));
assert!(!rendered1.entry_path.exists());
}

#[test]
fn launch_command_builds_expected_arguments() {
let adapter = VsCodeAdapter::new(IdeKind::VsCode, "/dummy/code");
let workspace = RenderedWorkspace {
entry_path: PathBuf::from("/path/to/project.code-workspace"),
generated_files: vec![],
};
let cmd = adapter.launch_command(&workspace);
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
assert!(args.contains(&"--new-window".to_string()));
assert!(args.contains(&"/path/to/project.code-workspace".to_string()));
}
}
2 changes: 1 addition & 1 deletion src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function Checkbox({
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary",
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:data-[state=unchecked]:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground group-has-[:focus-visible]/field-label:data-[state=checked]:border-primary dark:data-[state=checked]:bg-primary",
className
)}
{...props}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function DialogOverlay({
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
className
)}
{...props}
Expand All @@ -59,7 +59,7 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className
)}
{...props}
Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
align={align}
className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className )}
{...props}
/>
</DropdownMenuPrimitive.Portal>
Expand Down Expand Up @@ -223,7 +223,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
Expand All @@ -241,7 +241,7 @@ function DropdownMenuSubContent({
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn("z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
className={cn("z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className )}
{...props}
/>
)
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === "item-aligned"}
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
position={position}
align={align}
{...props}
Expand Down
Loading
Loading