Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/create/editor.ts
3583 views
1
/*
2
* cmd.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { CreateResult } from "./cmd-types.ts";
8
9
import { which } from "../../core/path.ts";
10
import {
11
isPositronTerminal,
12
isRStudioTerminal,
13
isVSCodeTerminal,
14
} from "../../core/platform.ts";
15
16
import { basename, dirname, join } from "../../deno_ral/path.ts";
17
import { existsSync } from "../../deno_ral/fs.ts";
18
import { isMac, isWindows } from "../../deno_ral/platform.ts";
19
import {
20
enforcer,
21
makeStringEnumTypeFunctions,
22
objectPredicate,
23
stringTypePredicate,
24
} from "../../typing/dynamic.ts";
25
import { call } from "../../deno_ral/process.ts";
26
27
export interface Editor {
28
// A short, command line friendly id
29
id: string;
30
31
// A display name
32
name: string;
33
34
// Whether this is being run from within the editor
35
// (e.g. from the vscode or rstudio terminal)
36
inEditor: boolean;
37
38
// Function that can be called to open the matched
39
// artifact in the editor
40
open: () => Promise<void>;
41
}
42
43
export const kEditorInfos: EditorInfo[] = [
44
positronEditorInfo(),
45
vscodeEditorInfo(),
46
rstudioEditorInfo(),
47
];
48
49
export async function scanForEditors(
50
editorInfos: EditorInfo[],
51
createResult: CreateResult,
52
) {
53
const editors: Editor[] = [];
54
for (const editorInfo of editorInfos) {
55
const editorPath = await findEditorPath(editorInfo.actions);
56
if (editorPath) {
57
editors.push({
58
id: editorInfo.id,
59
name: editorInfo.name,
60
open: editorInfo.open(editorPath, createResult),
61
inEditor: editorInfo.inEditor,
62
});
63
}
64
}
65
return editors;
66
}
67
68
interface EditorInfo {
69
// The identifier for this editor
70
id: string;
71
72
// The name of this editor
73
name: string;
74
75
// Actions that are used to scan for this editor
76
actions: ScanAction[];
77
78
// Whether this is being run from within the editor
79
// (e.g. from the vscode or rstudio terminal)
80
inEditor: boolean;
81
82
// Uses a path and artifact path to provide a function
83
// that can be used to open this editor to the given artifact
84
open: (path: string, createResult: CreateResult) => () => Promise<void>;
85
}
86
87
const scanActionActions = ["path", "which", "env"] as const;
88
type ScanActionAction = typeof scanActionActions[number];
89
90
type ScanAction = {
91
action: ScanActionAction;
92
arg: string;
93
filter?: (path: string) => string;
94
};
95
96
function vscodeEditorInfo(): EditorInfo {
97
const editorInfo: EditorInfo = {
98
id: "vscode",
99
name: "vscode",
100
open: (path: string, createResult: CreateResult) => {
101
const artifactPath = createResult.path;
102
const cwd = Deno.statSync(artifactPath).isDirectory
103
? artifactPath
104
: dirname(artifactPath);
105
106
return async () => {
107
await call(path, { args: [artifactPath], cwd });
108
};
109
},
110
inEditor: isVSCodeTerminal(),
111
actions: [],
112
};
113
114
if (isWindows) {
115
editorInfo.actions.push({
116
action: "which",
117
arg: "code.exe",
118
});
119
const pathActions: ScanAction[] = windowsAppPaths(
120
"Microsoft VS Code",
121
"code.exe",
122
).map((path) => ({
123
action: "path",
124
arg: path,
125
}));
126
editorInfo.actions.push(...pathActions);
127
} else if (isMac) {
128
editorInfo.actions.push({
129
action: "which",
130
arg: "code",
131
});
132
133
const pathActions: ScanAction[] = macosAppPaths(
134
"Visual Studio Code.app/Contents/Resources/app/bin/code",
135
).map((path) => ({
136
action: "path",
137
arg: path,
138
}));
139
editorInfo.actions.push(...pathActions);
140
} else {
141
editorInfo.actions.push({
142
action: "which",
143
arg: "code",
144
});
145
editorInfo.actions.push({
146
action: "path",
147
arg: "/snap/bin/code",
148
});
149
}
150
return editorInfo;
151
}
152
153
function positronEditorInfo(): EditorInfo {
154
const editorInfo: EditorInfo = {
155
id: "positron",
156
name: "positron",
157
open: (path: string, createResult: CreateResult) => {
158
const artifactPath = createResult.path;
159
const cwd = Deno.statSync(artifactPath).isDirectory
160
? artifactPath
161
: dirname(artifactPath);
162
163
return async () => {
164
await call(path, { args: [artifactPath], cwd });
165
};
166
},
167
inEditor: isPositronTerminal(),
168
actions: [],
169
};
170
171
if (isWindows) {
172
editorInfo.actions.push({
173
action: "which",
174
arg: "Positron.exe",
175
});
176
const pathActions: ScanAction[] = windowsAppPaths(
177
"Positron",
178
"Positron.exe",
179
).map(
180
(path) => ({
181
action: "path",
182
arg: path,
183
}),
184
);
185
editorInfo.actions.push(...pathActions);
186
} else if (isMac) {
187
editorInfo.actions.push({
188
action: "which",
189
arg: "positron",
190
});
191
192
const pathActions: ScanAction[] = macosAppPaths(
193
"Positron.app/Contents/Resources/app/bin/code",
194
).map((path) => {
195
return {
196
action: "path",
197
arg: path,
198
};
199
});
200
editorInfo.actions.push(...pathActions);
201
} else {
202
editorInfo.actions.push({
203
action: "which",
204
arg: "positron",
205
});
206
}
207
return editorInfo;
208
}
209
210
function rstudioEditorInfo(): EditorInfo {
211
const editorInfo: EditorInfo = {
212
id: "rstudio",
213
name: "RStudio",
214
open: (path: string, createResult: CreateResult) => {
215
return async () => {
216
const artifactPath = createResult.path;
217
// The directory that the artifact is in
218
const cwd = Deno.statSync(artifactPath).isDirectory
219
? artifactPath
220
: dirname(artifactPath);
221
222
// Write an rproj file for RStudio and open that
223
const artifactName = basename(artifactPath);
224
const rProjPath = join(cwd, `${artifactName}.Rproj`);
225
Deno.writeTextFileSync(rProjPath, kRProjContents);
226
227
const callCmd = path.endsWith(".app") && isMac
228
? ["open", "-na", path, "--args", rProjPath]
229
: [path, rProjPath];
230
231
const callPath = callCmd[0];
232
const args = callCmd.slice(1);
233
await call(callPath, { args, cwd });
234
};
235
},
236
inEditor: isRStudioTerminal(),
237
actions: [],
238
};
239
240
const rstudioExe = "rstudio.exe";
241
if (isWindows) {
242
editorInfo.actions.push({
243
action: "env",
244
arg: "RS_RPOSTBACK_PATH",
245
filter: (path: string) => {
246
return join(dirname(path), rstudioExe);
247
},
248
});
249
250
const paths: ScanAction[] = windowsAppPaths(
251
join("RStudio", "bin"),
252
rstudioExe,
253
).map((path) => ({
254
action: "path",
255
arg: path,
256
}));
257
editorInfo.actions.push(...paths);
258
} else if (isMac) {
259
const paths: ScanAction[] = macosAppPaths("RStudio.app").map((path) => ({
260
action: "path",
261
arg: path,
262
}));
263
editorInfo.actions.push(...paths);
264
} else {
265
editorInfo.actions.push({
266
action: "env",
267
arg: "RS_RPOSTBACK_PATH",
268
filter: (path: string) => {
269
return join(dirname(path), rstudioExe);
270
},
271
});
272
273
editorInfo.actions.push({
274
action: "path",
275
arg: "/usr/lib/rstudio/bin/rstudio",
276
});
277
editorInfo.actions.push({
278
action: "which",
279
arg: "rstudio",
280
});
281
}
282
return editorInfo;
283
}
284
285
// Write an rproj file to the cwd and open that
286
const kRProjContents = `Version: 1.0
287
288
RestoreWorkspace: Default
289
SaveWorkspace: Default
290
AlwaysSaveHistory: Default
291
292
EnableCodeIndexing: Yes
293
UseSpacesForTab: Yes
294
NumSpacesForTab: 2
295
Encoding: UTF-8
296
297
RnwWeave: Knitr
298
LaTeX: pdfLaTeX`;
299
300
async function findEditorPath(
301
actions: ScanAction[],
302
): Promise<string | undefined> {
303
for (const action of actions) {
304
const filter = action.filter || ((path) => {
305
return path;
306
});
307
switch (action.action) {
308
case "which": {
309
const path = await which(action.arg);
310
if (path) {
311
return filter(path);
312
}
313
break;
314
}
315
case "path":
316
if (existsSync(action.arg)) {
317
return filter(action.arg);
318
}
319
break;
320
case "env": {
321
const envValue = Deno.env.get(action.arg);
322
if (envValue) {
323
return filter(envValue);
324
}
325
}
326
}
327
}
328
// Couldn't find it, give up
329
return undefined;
330
}
331
332
function windowsAppPaths(folderName: string, command: string) {
333
const paths: string[] = [];
334
// Scan local app folder
335
const localAppData = Deno.env.get("LOCALAPPDATA");
336
if (localAppData) {
337
paths.push(join(localAppData, "Programs", folderName, command));
338
}
339
340
// Scan program files folder
341
const programFiles = Deno.env.get("PROGRAMFILES");
342
if (programFiles) {
343
paths.push(join(programFiles, folderName, command));
344
}
345
346
// Scan program files x86
347
const programFilesx86 = Deno.env.get("PROGRAMFILES(X86)");
348
if (programFilesx86) {
349
paths.push(join(programFilesx86, folderName, command));
350
}
351
352
return paths;
353
}
354
355
function macosAppPaths(appName: string) {
356
const paths: string[] = [];
357
paths.push(join("/Applications", appName));
358
const home = Deno.env.get("HOME");
359
if (home) {
360
paths.push(join(home, "Applications", appName));
361
}
362
return paths;
363
}
364
365