import { CreateResult } from "./cmd-types.ts";
import { which } from "../../core/path.ts";
import {
isPositronTerminal,
isRStudioTerminal,
isVSCodeTerminal,
} from "../../core/platform.ts";
import { basename, dirname, join } from "../../deno_ral/path.ts";
import { existsSync } from "../../deno_ral/fs.ts";
import { isMac, isWindows } from "../../deno_ral/platform.ts";
import {
enforcer,
makeStringEnumTypeFunctions,
objectPredicate,
stringTypePredicate,
} from "../../typing/dynamic.ts";
import { call } from "../../deno_ral/process.ts";
export interface Editor {
id: string;
name: string;
inEditor: boolean;
open: () => Promise<void>;
}
export const kEditorInfos: EditorInfo[] = [
positronEditorInfo(),
vscodeEditorInfo(),
rstudioEditorInfo(),
];
export async function scanForEditors(
editorInfos: EditorInfo[],
createResult: CreateResult,
) {
const editors: Editor[] = [];
for (const editorInfo of editorInfos) {
const editorPath = await findEditorPath(editorInfo.actions);
if (editorPath) {
editors.push({
id: editorInfo.id,
name: editorInfo.name,
open: editorInfo.open(editorPath, createResult),
inEditor: editorInfo.inEditor,
});
}
}
return editors;
}
interface EditorInfo {
id: string;
name: string;
actions: ScanAction[];
inEditor: boolean;
open: (path: string, createResult: CreateResult) => () => Promise<void>;
}
const scanActionActions = ["path", "which", "env"] as const;
type ScanActionAction = typeof scanActionActions[number];
type ScanAction = {
action: ScanActionAction;
arg: string;
filter?: (path: string) => string;
};
function vscodeEditorInfo(): EditorInfo {
const editorInfo: EditorInfo = {
id: "vscode",
name: "vscode",
open: (path: string, createResult: CreateResult) => {
const artifactPath = createResult.path;
const cwd = Deno.statSync(artifactPath).isDirectory
? artifactPath
: dirname(artifactPath);
return async () => {
await call(path, { args: [artifactPath], cwd });
};
},
inEditor: isVSCodeTerminal(),
actions: [],
};
if (isWindows) {
editorInfo.actions.push({
action: "which",
arg: "code.exe",
});
const pathActions: ScanAction[] = windowsAppPaths(
"Microsoft VS Code",
"code.exe",
).map((path) => ({
action: "path",
arg: path,
}));
editorInfo.actions.push(...pathActions);
} else if (isMac) {
editorInfo.actions.push({
action: "which",
arg: "code",
});
const pathActions: ScanAction[] = macosAppPaths(
"Visual Studio Code.app/Contents/Resources/app/bin/code",
).map((path) => ({
action: "path",
arg: path,
}));
editorInfo.actions.push(...pathActions);
} else {
editorInfo.actions.push({
action: "which",
arg: "code",
});
editorInfo.actions.push({
action: "path",
arg: "/snap/bin/code",
});
}
return editorInfo;
}
function positronEditorInfo(): EditorInfo {
const editorInfo: EditorInfo = {
id: "positron",
name: "positron",
open: (path: string, createResult: CreateResult) => {
const artifactPath = createResult.path;
const cwd = Deno.statSync(artifactPath).isDirectory
? artifactPath
: dirname(artifactPath);
return async () => {
await call(path, { args: [artifactPath], cwd });
};
},
inEditor: isPositronTerminal(),
actions: [],
};
if (isWindows) {
editorInfo.actions.push({
action: "which",
arg: "Positron.exe",
});
const pathActions: ScanAction[] = windowsAppPaths(
"Positron",
"Positron.exe",
).map(
(path) => ({
action: "path",
arg: path,
}),
);
editorInfo.actions.push(...pathActions);
} else if (isMac) {
editorInfo.actions.push({
action: "which",
arg: "positron",
});
const pathActions: ScanAction[] = macosAppPaths(
"Positron.app/Contents/Resources/app/bin/code",
).map((path) => {
return {
action: "path",
arg: path,
};
});
editorInfo.actions.push(...pathActions);
} else {
editorInfo.actions.push({
action: "which",
arg: "positron",
});
}
return editorInfo;
}
function rstudioEditorInfo(): EditorInfo {
const editorInfo: EditorInfo = {
id: "rstudio",
name: "RStudio",
open: (path: string, createResult: CreateResult) => {
return async () => {
const artifactPath = createResult.path;
const cwd = Deno.statSync(artifactPath).isDirectory
? artifactPath
: dirname(artifactPath);
const artifactName = basename(artifactPath);
const rProjPath = join(cwd, `${artifactName}.Rproj`);
Deno.writeTextFileSync(rProjPath, kRProjContents);
const callCmd = path.endsWith(".app") && isMac
? ["open", "-na", path, "--args", rProjPath]
: [path, rProjPath];
const callPath = callCmd[0];
const args = callCmd.slice(1);
await call(callPath, { args, cwd });
};
},
inEditor: isRStudioTerminal(),
actions: [],
};
const rstudioExe = "rstudio.exe";
if (isWindows) {
editorInfo.actions.push({
action: "env",
arg: "RS_RPOSTBACK_PATH",
filter: (path: string) => {
return join(dirname(path), rstudioExe);
},
});
const paths: ScanAction[] = windowsAppPaths(
join("RStudio", "bin"),
rstudioExe,
).map((path) => ({
action: "path",
arg: path,
}));
editorInfo.actions.push(...paths);
} else if (isMac) {
const paths: ScanAction[] = macosAppPaths("RStudio.app").map((path) => ({
action: "path",
arg: path,
}));
editorInfo.actions.push(...paths);
} else {
editorInfo.actions.push({
action: "env",
arg: "RS_RPOSTBACK_PATH",
filter: (path: string) => {
return join(dirname(path), rstudioExe);
},
});
editorInfo.actions.push({
action: "path",
arg: "/usr/lib/rstudio/bin/rstudio",
});
editorInfo.actions.push({
action: "which",
arg: "rstudio",
});
}
return editorInfo;
}
const kRProjContents = `Version: 1.0
RestoreWorkspace: Default
SaveWorkspace: Default
AlwaysSaveHistory: Default
EnableCodeIndexing: Yes
UseSpacesForTab: Yes
NumSpacesForTab: 2
Encoding: UTF-8
RnwWeave: Knitr
LaTeX: pdfLaTeX`;
async function findEditorPath(
actions: ScanAction[],
): Promise<string | undefined> {
for (const action of actions) {
const filter = action.filter || ((path) => {
return path;
});
switch (action.action) {
case "which": {
const path = await which(action.arg);
if (path) {
return filter(path);
}
break;
}
case "path":
if (existsSync(action.arg)) {
return filter(action.arg);
}
break;
case "env": {
const envValue = Deno.env.get(action.arg);
if (envValue) {
return filter(envValue);
}
}
}
}
return undefined;
}
function windowsAppPaths(folderName: string, command: string) {
const paths: string[] = [];
const localAppData = Deno.env.get("LOCALAPPDATA");
if (localAppData) {
paths.push(join(localAppData, "Programs", folderName, command));
}
const programFiles = Deno.env.get("PROGRAMFILES");
if (programFiles) {
paths.push(join(programFiles, folderName, command));
}
const programFilesx86 = Deno.env.get("PROGRAMFILES(X86)");
if (programFilesx86) {
paths.push(join(programFilesx86, folderName, command));
}
return paths;
}
function macosAppPaths(appName: string) {
const paths: string[] = [];
paths.push(join("/Applications", appName));
const home = Deno.env.get("HOME");
if (home) {
paths.push(join(home, "Applications", appName));
}
return paths;
}