@parallelworks/ui 0.15.0 → 0.17.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/dist/chunks/{CodePreviewEditor-DuMZMA_k.js → CodePreviewEditor-UAtnpmOV.js} +1 -1
- package/dist/chunks/{CreateModal-B7smkHkv.js → CreateModal-BbV5cTj7.js} +3 -3
- package/dist/chunks/{Monaco-BXDH2jVj.js → Monaco-DVyIpyv4.js} +485 -172
- package/dist/chunks/{PageHeader-CoZWF0pJ.js → PageHeader-MxuOs9cH.js} +1 -1
- package/dist/chunks/{Provider-CbFVFB7O.js → Provider-DHmZ3v9i.js} +28 -2
- package/dist/chunks/{StatusBadge-zvEtK16a.js → StatusBadge-C1Blx_cW.js} +23 -6
- package/dist/chunks/{SwitchToggle-3V836Cn7.js → SwitchToggle-BRQoEwLZ.js} +7 -4
- package/dist/chunks/{Table-CutP1bRt.js → Table-C6JdFFiJ.js} +1 -1
- package/dist/chunks/{editor-Ko3zX22Y.js → editor-mNznodJN.js} +2 -2
- package/dist/chunks/{list-CduoUsMW.js → list-7k0eaI5N.js} +21 -19
- package/dist/components/Button.d.ts +2 -1
- package/dist/components/Provider.d.ts +57 -2
- package/dist/components/SectionHeader.d.ts +3 -1
- package/dist/components/StatusBadge.d.ts +7 -2
- package/dist/components/relativeTime.d.ts +15 -0
- package/dist/editor/index.js +1 -1
- package/dist/editor/usesGitlab.d.ts +97 -0
- package/dist/file-explorer/index.js +253 -253
- package/dist/file-explorer/lib/paging.d.ts +27 -28
- package/dist/file-explorer/lib/types.d.ts +0 -1
- package/dist/file-explorer/lib/utils.d.ts +16 -5
- package/dist/form/index.js +5 -5
- package/dist/form/types/workflow-schema.d.ts +38 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +42 -11
- package/dist/list/ListPage.d.ts +3 -1
- package/dist/list/ListPager.d.ts +3 -1
- package/dist/list/ListTable.d.ts +2 -1
- package/dist/list/index.js +2 -2
- package/dist/list/listView.d.ts +1 -1
- package/dist/logviewer/index.js +1 -1
- package/dist/styles/theme.css +14 -12
- package/dist/styles.css +1 -1
- package/dist/workflow/DependencyGraph.d.ts +2 -0
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +3 -2
- package/dist/workflow/types.d.ts +5 -0
- package/package.json +4 -4
|
@@ -1,16 +1,307 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as useWorkflowJsonResolver, s as useRepoSuggestions } from "./Provider-DHmZ3v9i.js";
|
|
2
2
|
import { t as useCssIsDark } from "./useCssIsDark-DHr05dG2.js";
|
|
3
|
-
import { n as defineEditorThemes, r as getThemeName, t as setupMonacoWorkers } from "./editor-
|
|
3
|
+
import { n as defineEditorThemes, r as getThemeName, t as setupMonacoWorkers } from "./editor-mNznodJN.js";
|
|
4
4
|
import cx from "classnames";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
-
import { useEffect, useRef } from "react";
|
|
6
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
7
7
|
import { dumpYaml } from "@parallelworks/workflow-parser";
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
9
|
import * as monaco from "monaco-editor";
|
|
10
10
|
import { configureMonacoYaml } from "monaco-yaml";
|
|
11
|
+
//#region src/editor/usesGitlab.ts
|
|
12
|
+
/** Splits GROUP/PROJECT@REF; the ref is null when no @ has been typed yet. */
|
|
13
|
+
function splitAtRef(typed) {
|
|
14
|
+
const at = typed.indexOf("@");
|
|
15
|
+
if (at === -1) return [typed, null];
|
|
16
|
+
return [typed.slice(0, at), typed.slice(at + 1)];
|
|
17
|
+
}
|
|
18
|
+
/** What to insert for a chosen project, given what is already typed. A typed prefix is
|
|
19
|
+
* completed like the github one; a mid-path match has to replace what is there. */
|
|
20
|
+
function projectInsert(typed, path) {
|
|
21
|
+
if (path.toLowerCase().startsWith(typed.toLowerCase())) return {
|
|
22
|
+
text: path.slice(typed.length),
|
|
23
|
+
replace: false
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
text: path,
|
|
27
|
+
replace: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Indent of a line, or -1 when it is blank. */
|
|
31
|
+
function indentOf(content) {
|
|
32
|
+
if (content.trim() === "") return -1;
|
|
33
|
+
return content.length - content.trimStart().length;
|
|
34
|
+
}
|
|
35
|
+
/** Every line belonging to the step a uses: is part of, in order. */
|
|
36
|
+
function stepBody(model, usesLine, maxLines = 40) {
|
|
37
|
+
const keyIndent = model.getLineContent(usesLine).match(/^(\s*(?:-\s+)?)/)?.[1]?.length ?? 0;
|
|
38
|
+
const out = [];
|
|
39
|
+
for (let ln = usesLine + 1; ln <= usesLine + maxLines; ln++) {
|
|
40
|
+
let content;
|
|
41
|
+
try {
|
|
42
|
+
content = model.getLineContent(ln);
|
|
43
|
+
} catch {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
const indent = indentOf(content);
|
|
47
|
+
if (indent === -1) continue;
|
|
48
|
+
if (indent < keyIndent) break;
|
|
49
|
+
out.push({
|
|
50
|
+
line: ln,
|
|
51
|
+
content
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** The $host the step pins, or undefined. Quotes and a trailing comment are stripped,
|
|
57
|
+
* so a pinned host is recognised however it was written. */
|
|
58
|
+
function stepHostAbove(model, usesLine, maxLines = 40) {
|
|
59
|
+
for (const { content } of stepBody(model, usesLine, maxLines)) {
|
|
60
|
+
const match = content.match(/^\s*\$host\s*:\s*(.+?)\s*$/);
|
|
61
|
+
if (match) return unquoteScalar(match[1] ?? "");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** A YAML scalar as written: quotes removed, a trailing comment dropped. */
|
|
65
|
+
function unquoteScalar(raw) {
|
|
66
|
+
let value = raw.trim();
|
|
67
|
+
const quoted = value.match(/^(['"])(.*)\1\s*(?:#.*)?$/);
|
|
68
|
+
if (quoted) return quoted[2] ?? "";
|
|
69
|
+
const hash = value.indexOf(" #");
|
|
70
|
+
if (hash !== -1) value = value.slice(0, hash);
|
|
71
|
+
return value.trim();
|
|
72
|
+
}
|
|
73
|
+
/** How to add `$host` to a step. Always an edit of its own: monaco re-indents a
|
|
74
|
+
* multi-line insertText, and the cursor has to stay after the project. */
|
|
75
|
+
function hostCompletion(model, usesLine, host) {
|
|
76
|
+
const none = {
|
|
77
|
+
appendText: "",
|
|
78
|
+
edits: []
|
|
79
|
+
};
|
|
80
|
+
if (!host || host === "gitlab.com") return none;
|
|
81
|
+
const usesContent = model.getLineContent(usesLine);
|
|
82
|
+
const keyIndent = " ".repeat(usesContent.match(/^(\s*(?:-\s+)?)uses/)?.[1]?.length ?? indentOf(usesContent));
|
|
83
|
+
for (const { line, content } of stepBody(model, usesLine)) if (/^\s*with\s*:\s*$/.test(content)) {
|
|
84
|
+
const withIndent = content.match(/^(\s*)/)?.[1] ?? keyIndent;
|
|
85
|
+
return place(model, line + 1, `${withIndent} $host: ${host}`);
|
|
86
|
+
}
|
|
87
|
+
return place(model, usesLine + 1, `${keyIndent}with:\n${keyIndent} $host: ${host}`);
|
|
88
|
+
}
|
|
89
|
+
/** Whole lines that belong before `line`. With nothing there to sit above, they ride along
|
|
90
|
+
* in the completion's own text: a second edit would share the last line's end offset with
|
|
91
|
+
* the item insert, and monaco decides which of the two lands first. */
|
|
92
|
+
function place(model, line, text) {
|
|
93
|
+
if (line > model.getLineCount()) return {
|
|
94
|
+
appendText: `\n${text}`,
|
|
95
|
+
edits: []
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
appendText: "",
|
|
99
|
+
edits: [lineEdit(model, line, text)]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** An insert of whole lines before `line`, which the completion cannot shift because it
|
|
103
|
+
* never spans a newline. */
|
|
104
|
+
function lineEdit(model, line, text) {
|
|
105
|
+
return {
|
|
106
|
+
range: {
|
|
107
|
+
startLineNumber: line,
|
|
108
|
+
startColumn: 1,
|
|
109
|
+
endLineNumber: line,
|
|
110
|
+
endColumn: 1
|
|
111
|
+
},
|
|
112
|
+
text: `${text}\n`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** A trailing slash or @ after a known project is the ref separator, the way a trailing
|
|
116
|
+
* slash is for github/OWNER/REPO. The slash is swapped for @ when a ref is accepted. */
|
|
117
|
+
function refTrigger(typed, knownPaths) {
|
|
118
|
+
const at = typed.indexOf("@");
|
|
119
|
+
if (at !== -1) return typed.slice(at + 1) === "" ? {
|
|
120
|
+
projectPath: typed.slice(0, at),
|
|
121
|
+
replaceSlash: false
|
|
122
|
+
} : void 0;
|
|
123
|
+
if (!typed.endsWith("/")) return;
|
|
124
|
+
const projectPath = typed.slice(0, -1);
|
|
125
|
+
return knownPaths.includes(projectPath) ? {
|
|
126
|
+
projectPath,
|
|
127
|
+
replaceSlash: true
|
|
128
|
+
} : void 0;
|
|
129
|
+
}
|
|
130
|
+
/** What to ask the platform for when completing a project path: everything before the
|
|
131
|
+
* first slash. Typing deeper then filters that answer instead of asking again. */
|
|
132
|
+
function projectSearchKey(typed) {
|
|
133
|
+
const slash = typed.indexOf("/");
|
|
134
|
+
return slash === -1 ? typed : typed.slice(0, slash);
|
|
135
|
+
}
|
|
136
|
+
/** Where a `$yaml:`/`$thumbnail:` value's files come from: the nearest `uses:` above,
|
|
137
|
+
* addressed the way the platform's file endpoint expects. A gitlab/ path carries no
|
|
138
|
+
* host, so the step's $host supplies it. */
|
|
139
|
+
function usesFileSource(model, fromLine, maxLines = 30) {
|
|
140
|
+
for (let ln = fromLine - 1; ln >= Math.max(fromLine - maxLines, 1); ln--) {
|
|
141
|
+
const uses = model.getLineContent(ln).trim().match(/^(?:-\s+)?uses\s*:\s*(\S+)\s*$/);
|
|
142
|
+
if (!uses) continue;
|
|
143
|
+
const [target, ref] = splitAtRef(uses[1] ?? "");
|
|
144
|
+
const github = target.match(/^github\/([^/]+\/.+)$/);
|
|
145
|
+
if (github) return {
|
|
146
|
+
repo: github[1] ?? "",
|
|
147
|
+
ref: ref ?? ""
|
|
148
|
+
};
|
|
149
|
+
const gitlab = target.match(/^gitlab\/(.+)$/);
|
|
150
|
+
if (gitlab) return {
|
|
151
|
+
repo: `https://${stepHostAbove(model, ln) ?? "gitlab.com"}/${gitlab[1] ?? ""}`,
|
|
152
|
+
ref: ref ?? ""
|
|
153
|
+
};
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
var FILE_VALUE_KINDS = {
|
|
158
|
+
$yaml: {
|
|
159
|
+
detail: "yaml file",
|
|
160
|
+
keep: (path) => path.endsWith(".yaml") || path.endsWith(".yml")
|
|
161
|
+
},
|
|
162
|
+
$thumbnail: {
|
|
163
|
+
detail: "image file",
|
|
164
|
+
keep: (path) => [
|
|
165
|
+
".png",
|
|
166
|
+
".jpg",
|
|
167
|
+
".jpeg",
|
|
168
|
+
".gif",
|
|
169
|
+
".svg",
|
|
170
|
+
".webp"
|
|
171
|
+
].some((ext) => path.toLowerCase().endsWith(ext))
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/** Which of the two file-valued keys the cursor sits on, if either. */
|
|
175
|
+
function fileValueKey(textUntilCursor) {
|
|
176
|
+
return textUntilCursor.match(/^\s*(\$yaml|\$thumbnail)\s*:\s*$/)?.[1] ?? void 0;
|
|
177
|
+
}
|
|
178
|
+
/** The files worth offering for a `$yaml:`/`$thumbnail:` value, from whichever
|
|
179
|
+
* repository the step's `uses:` names. Empty when the line is not one of those keys,
|
|
180
|
+
* no `uses:` is above it, or the tree cannot be read. */
|
|
181
|
+
async function fileValueSuggestions(model, line, textUntilCursor, loadTree) {
|
|
182
|
+
const key = fileValueKey(textUntilCursor);
|
|
183
|
+
if (!key) return [];
|
|
184
|
+
const source = usesFileSource(model, line);
|
|
185
|
+
if (!source) return [];
|
|
186
|
+
const { detail, keep } = FILE_VALUE_KINDS[key];
|
|
187
|
+
return (await loadTree(source.repo, source.ref)).filter((entry) => entry.type === "blob" && keep(entry.path)).map((entry) => ({
|
|
188
|
+
path: entry.path,
|
|
189
|
+
detail
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
/** Every $-prefixed key a step already sets, mapped to its value. Nothing already
|
|
193
|
+
* written is offered a second time, and an existing one must not suppress the rest. */
|
|
194
|
+
function specialKeysNearUses(model, usesLineNumber) {
|
|
195
|
+
const found = /* @__PURE__ */ new Map();
|
|
196
|
+
const totalLines = model.getLineCount();
|
|
197
|
+
for (let ln = usesLineNumber + 1; ln <= Math.min(usesLineNumber + 20, totalLines); ln++) {
|
|
198
|
+
const content = model.getLineContent(ln).trim();
|
|
199
|
+
if (content === "") continue;
|
|
200
|
+
if (/^-\s/.test(content)) break;
|
|
201
|
+
const match = content.match(/^(\$[A-Za-z]+)\s*:\s*(.*)$/);
|
|
202
|
+
if (match) found.set(match[1], match[2].trim());
|
|
203
|
+
}
|
|
204
|
+
return found;
|
|
205
|
+
}
|
|
206
|
+
/** The same, for a with: block located by its own indentation. */
|
|
207
|
+
function specialKeysInWithBlock(model, withLineNumber) {
|
|
208
|
+
const found = /* @__PURE__ */ new Map();
|
|
209
|
+
const withIndent = model.getLineContent(withLineNumber).match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
210
|
+
const totalLines = model.getLineCount();
|
|
211
|
+
for (let ln = withLineNumber + 1; ln <= totalLines; ln++) {
|
|
212
|
+
const line = model.getLineContent(ln);
|
|
213
|
+
if (line.trim() === "") continue;
|
|
214
|
+
if ((line.match(/^(\s*)/)?.[1]?.length ?? 0) <= withIndent) break;
|
|
215
|
+
const match = line.match(/^\s*(\$[A-Za-z]+)\s*:\s*(.*)$/);
|
|
216
|
+
if (match) found.set(match[1], match[2].trim());
|
|
217
|
+
}
|
|
218
|
+
return found;
|
|
219
|
+
}
|
|
220
|
+
/** Text to be inserted as a snippet, with the characters monaco's snippet parser would
|
|
221
|
+
* otherwise read as placeholders escaped. */
|
|
222
|
+
function snippetLiteral(text) {
|
|
223
|
+
return text.replace(/[\\$}]/g, "\\$&");
|
|
224
|
+
}
|
|
225
|
+
/** How a completion inserts its text when a `$host` block has to travel with it. The
|
|
226
|
+
* cursor stop is what keeps the caret on the uses: line rather than after the block;
|
|
227
|
+
* without a block there is nothing to escape and it stays a plain insert. */
|
|
228
|
+
function completionInsert(text, appendText) {
|
|
229
|
+
if (!appendText) return {
|
|
230
|
+
insertText: text,
|
|
231
|
+
isSnippet: false
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
insertText: `${snippetLiteral(text)}$0${snippetLiteral(appendText)}`,
|
|
235
|
+
isSnippet: true
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
11
239
|
//#region src/editor/Monaco.tsx
|
|
12
240
|
setupMonacoWorkers();
|
|
13
241
|
var activeWorkflowJsonResolver;
|
|
242
|
+
var activeRepoSuggestions;
|
|
243
|
+
var suggestionCache = /* @__PURE__ */ new Map();
|
|
244
|
+
var CACHE_MS = 3e4;
|
|
245
|
+
var OWNER_CACHE_MS = 5 * 6e4;
|
|
246
|
+
var asSnippet = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet | monaco.languages.CompletionItemInsertTextRule.KeepWhitespace;
|
|
247
|
+
var MAX_PROJECT_SUGGESTIONS = 200;
|
|
248
|
+
async function cached(key, fallback, load, ttl = CACHE_MS) {
|
|
249
|
+
if (!activeRepoSuggestions) return fallback;
|
|
250
|
+
const hit = suggestionCache.get(key);
|
|
251
|
+
if (hit && Date.now() - hit.at < ttl) try {
|
|
252
|
+
return await hit.value;
|
|
253
|
+
} catch {
|
|
254
|
+
return fallback;
|
|
255
|
+
}
|
|
256
|
+
if (suggestionCache.size > 100) suggestionCache.clear();
|
|
257
|
+
const value = load();
|
|
258
|
+
suggestionCache.set(key, {
|
|
259
|
+
at: Date.now(),
|
|
260
|
+
value
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
return await value;
|
|
264
|
+
} catch {
|
|
265
|
+
suggestionCache.delete(key);
|
|
266
|
+
return fallback;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Refs of a repository, or [] when the host bound nothing or the call fails. */
|
|
270
|
+
async function repoRefs(repo) {
|
|
271
|
+
return cached(`refs\u0000${repo}`, [], () => activeRepoSuggestions.refs(repo));
|
|
272
|
+
}
|
|
273
|
+
/** Repositories under an owner, or every one the account can reach when the owner is
|
|
274
|
+
* empty. [] when the host bound nothing. */
|
|
275
|
+
async function repoOwnerRepos(owner) {
|
|
276
|
+
return cached(`repos\u0000${owner}`, [], () => activeRepoSuggestions.repos(owner), OWNER_CACHE_MS);
|
|
277
|
+
}
|
|
278
|
+
/** Owners a reference can name: accounts and organizations, or GitLab groups. */
|
|
279
|
+
async function repoOwners(provider, search, host) {
|
|
280
|
+
return cached(`owners\u0000${provider}\u0000${host ?? ""}\u0000${search}`, [], () => activeRepoSuggestions.owners(provider, search, host), OWNER_CACHE_MS);
|
|
281
|
+
}
|
|
282
|
+
/** Projects matching what is typed, fetched once per group rather than once per
|
|
283
|
+
* keystroke. The platform caps its answer, so a group that fills the cap is re-asked
|
|
284
|
+
* with the full path instead of filtering an already-truncated list. */
|
|
285
|
+
async function matchingProjects(typed, host) {
|
|
286
|
+
const key = projectSearchKey(typed);
|
|
287
|
+
const byGroup = await gitlabProjects(key, host);
|
|
288
|
+
const wanted = typed.toLowerCase();
|
|
289
|
+
const narrowed = byGroup.filter((project) => project.path.toLowerCase().includes(wanted));
|
|
290
|
+
if (key !== typed && byGroup.length >= MAX_PROJECT_SUGGESTIONS) return gitlabProjects(typed, host);
|
|
291
|
+
return narrowed;
|
|
292
|
+
}
|
|
293
|
+
/** GitLab projects matching a partial path, across every connected server. */
|
|
294
|
+
async function gitlabProjects(search, host) {
|
|
295
|
+
return cached(`gitlab\u0000${host ?? ""}\u0000${search}`, [], () => activeRepoSuggestions.gitlabProjects(search, host), OWNER_CACHE_MS);
|
|
296
|
+
}
|
|
297
|
+
/** One file's contents, or '' on the same terms. */
|
|
298
|
+
async function repoFile(repo, ref, path) {
|
|
299
|
+
return cached(`file\u0000${repo}\u0000${ref}\u0000${path}`, "", () => activeRepoSuggestions.file(repo, ref, path));
|
|
300
|
+
}
|
|
301
|
+
/** Tree entries of a repository, or [] on the same terms. */
|
|
302
|
+
async function repoFiles(repo, ref, path) {
|
|
303
|
+
return cached(`files\u0000${repo}\u0000${ref}\u0000${path}`, [], () => activeRepoSuggestions.files(repo, ref, path));
|
|
304
|
+
}
|
|
14
305
|
var monacoWithWorkerBridge = {
|
|
15
306
|
...monaco,
|
|
16
307
|
editor: {
|
|
@@ -47,16 +338,23 @@ function readExecuteInputs(doc) {
|
|
|
47
338
|
const inputs = execute["inputs"];
|
|
48
339
|
return isWorkflowInputs(inputs) ? inputs : {};
|
|
49
340
|
}
|
|
341
|
+
function editorText(value) {
|
|
342
|
+
return value.replace(/\r/g, "");
|
|
343
|
+
}
|
|
50
344
|
var noop = () => {};
|
|
51
345
|
function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop, width = "100%", height = "100%", path = "file:///default", marketplaceItems = [], workflows = [], ...options }) {
|
|
52
346
|
const isDark = useCssIsDark();
|
|
53
347
|
const readOnly = disabled || options.readOnly;
|
|
54
348
|
const resolveWorkflowJson = useWorkflowJsonResolver();
|
|
349
|
+
const repoSuggestions = useRepoSuggestions();
|
|
55
350
|
const monacoRef = useRef(null);
|
|
56
351
|
const isSyncingValueRef = useRef(false);
|
|
57
352
|
useEffect(() => {
|
|
58
353
|
activeWorkflowJsonResolver = resolveWorkflowJson;
|
|
59
354
|
}, [resolveWorkflowJson]);
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
activeRepoSuggestions = repoSuggestions;
|
|
357
|
+
}, [repoSuggestions]);
|
|
60
358
|
useEffect(() => {
|
|
61
359
|
if (!monacoRef.current) return;
|
|
62
360
|
defineEditorThemes(monaco.editor);
|
|
@@ -67,12 +365,13 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
67
365
|
isDark,
|
|
68
366
|
readOnly
|
|
69
367
|
]);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
368
|
+
useLayoutEffect(() => {
|
|
369
|
+
const model = monacoRef.current?.getModel();
|
|
370
|
+
if (!model || options.value === void 0 || editorText(model.getValue()) === editorText(options.value)) return;
|
|
371
|
+
isSyncingValueRef.current = true;
|
|
372
|
+
try {
|
|
75
373
|
model.setValue(options.value);
|
|
374
|
+
} finally {
|
|
76
375
|
isSyncingValueRef.current = false;
|
|
77
376
|
}
|
|
78
377
|
}, [options.value]);
|
|
@@ -99,7 +398,7 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
99
398
|
]);
|
|
100
399
|
/** Strips CR (\r) — Windows line endings break bash scripts. */
|
|
101
400
|
const onChangeWrapper = (value) => {
|
|
102
|
-
value = value
|
|
401
|
+
value = editorText(value);
|
|
103
402
|
onChange(value);
|
|
104
403
|
};
|
|
105
404
|
return /* @__PURE__ */ jsx("div", {
|
|
@@ -164,8 +463,9 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
164
463
|
const workflowIndex = textUntilPos.lastIndexOf("workflow/");
|
|
165
464
|
const pwIndex = textUntilPos.lastIndexOf("parallelworks/");
|
|
166
465
|
const githubIndex = textUntilPos.lastIndexOf("github/");
|
|
466
|
+
const gitlabIndex = textUntilPos.lastIndexOf("gitlab/");
|
|
167
467
|
const usesText = textUntilPos.match(/^\s*(?:-\s+)?uses\s*:\s*([^\n\r]*)$/)?.[1]?.trim() ?? "";
|
|
168
|
-
if (marketIndex === -1 && workflowIndex === -1 && pwIndex === -1 && githubIndex === -1) {
|
|
468
|
+
if (marketIndex === -1 && workflowIndex === -1 && pwIndex === -1 && githubIndex === -1 && gitlabIndex === -1) {
|
|
169
469
|
if (usesText.trim().length > 0) return { suggestions };
|
|
170
470
|
return { suggestions: [
|
|
171
471
|
{
|
|
@@ -186,7 +486,14 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
186
486
|
label: "github",
|
|
187
487
|
sortText: "4",
|
|
188
488
|
insertText: "github",
|
|
189
|
-
detail: "use a workflow from a
|
|
489
|
+
detail: "use a workflow from a GitHub repository",
|
|
490
|
+
...defaultItem
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
label: "gitlab",
|
|
494
|
+
sortText: "5",
|
|
495
|
+
insertText: "gitlab",
|
|
496
|
+
detail: "use a workflow from a GitLab project",
|
|
190
497
|
...defaultItem
|
|
191
498
|
}
|
|
192
499
|
] };
|
|
@@ -195,7 +502,8 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
195
502
|
"marketplace/",
|
|
196
503
|
"workflow/",
|
|
197
504
|
"parallelworks/",
|
|
198
|
-
"github/"
|
|
505
|
+
"github/",
|
|
506
|
+
"gitlab/"
|
|
199
507
|
].includes(usesText);
|
|
200
508
|
if (workflowIndex !== -1 && noExtra) {
|
|
201
509
|
if (lineContent.substring(workflowIndex, position.column - 1).split("/").length === 2) suggestions = workflows.map((item) => {
|
|
@@ -238,20 +546,25 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
238
546
|
label: "Type a GitHub username or organization",
|
|
239
547
|
insertText: "",
|
|
240
548
|
detail: "e.g. github/myorg/",
|
|
549
|
+
sortText: "0",
|
|
241
550
|
...defaultItem
|
|
242
|
-
}
|
|
551
|
+
}, ...(await repoOwners("github", "")).map((owner, index) => ({
|
|
552
|
+
label: owner.name,
|
|
553
|
+
filterText: owner.name,
|
|
554
|
+
insertText: owner.name,
|
|
555
|
+
detail: owner.kind,
|
|
556
|
+
sortText: `1${String(index).padStart(4, "0")}`,
|
|
557
|
+
...defaultItem
|
|
558
|
+
}))];
|
|
243
559
|
else if (!afterGithub.includes("/")) {} else {
|
|
244
560
|
const ghOwner = afterGithub.split("/")[0];
|
|
245
561
|
const rest = afterGithub.split("/").slice(1).join("/");
|
|
246
|
-
if (rest === "")
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
...defaultItem
|
|
253
|
-
}));
|
|
254
|
-
} catch {}
|
|
562
|
+
if (rest === "") suggestions = (await repoOwnerRepos(ghOwner ?? "")).map((r) => ({
|
|
563
|
+
label: r.description ? `${r.name} — ${r.description}` : r.name,
|
|
564
|
+
insertText: r.name,
|
|
565
|
+
detail: r.description || (r.private ? "private repository" : "repository"),
|
|
566
|
+
...defaultItem
|
|
567
|
+
}));
|
|
255
568
|
else {
|
|
256
569
|
let ghRepo = "";
|
|
257
570
|
let isRefTrigger = false;
|
|
@@ -279,46 +592,105 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
279
592
|
},
|
|
280
593
|
text: "@"
|
|
281
594
|
}] : [];
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
label: b.name,
|
|
292
|
-
insertText: b.name,
|
|
293
|
-
detail: "branch",
|
|
294
|
-
additionalTextEdits: slashToAtEdit,
|
|
295
|
-
...defaultItem
|
|
296
|
-
})));
|
|
297
|
-
}
|
|
298
|
-
if (tagResp.ok) {
|
|
299
|
-
const tags = await tagResp.json();
|
|
300
|
-
suggestions.push(...tags.map((t) => ({
|
|
301
|
-
label: t.name,
|
|
302
|
-
insertText: t.name,
|
|
303
|
-
detail: "tag",
|
|
304
|
-
additionalTextEdits: slashToAtEdit,
|
|
305
|
-
...defaultItem
|
|
306
|
-
})));
|
|
307
|
-
}
|
|
308
|
-
if (commitResp.ok) {
|
|
309
|
-
const commits = await commitResp.json();
|
|
310
|
-
suggestions.push(...commits.map((c) => ({
|
|
311
|
-
label: `${c.sha.slice(0, 7)} — ${c.commit.message.split("\n")[0]}`,
|
|
312
|
-
insertText: c.sha,
|
|
313
|
-
detail: "commit",
|
|
314
|
-
additionalTextEdits: slashToAtEdit,
|
|
315
|
-
...defaultItem
|
|
316
|
-
})));
|
|
317
|
-
}
|
|
318
|
-
} catch {}
|
|
595
|
+
const refs = await repoRefs(`${ghOwner}/${ghRepo}`);
|
|
596
|
+
suggestions.push(...refs.map((ref, index) => ({
|
|
597
|
+
label: ref.kind === "commit" ? ref.name.slice(0, 7) : ref.name,
|
|
598
|
+
insertText: ref.name,
|
|
599
|
+
detail: ref.detail || ref.kind,
|
|
600
|
+
sortText: String(index).padStart(4, "0"),
|
|
601
|
+
additionalTextEdits: slashToAtEdit,
|
|
602
|
+
...defaultItem
|
|
603
|
+
})));
|
|
319
604
|
}
|
|
320
605
|
}
|
|
321
606
|
}
|
|
607
|
+
} else if (gitlabIndex !== -1) {
|
|
608
|
+
const typed = usesText.replace("gitlab/", "");
|
|
609
|
+
const pinned = stepHostAbove(model, position.lineNumber);
|
|
610
|
+
const projects = typed === "" ? [] : await matchingProjects(typed.replace(/[@/]$/, ""), pinned);
|
|
611
|
+
const trigger = refTrigger(typed, projects.map((project) => project.path));
|
|
612
|
+
if (trigger) {
|
|
613
|
+
const slashToAtEdit = trigger.replaceSlash ? [{
|
|
614
|
+
range: {
|
|
615
|
+
startLineNumber: position.lineNumber,
|
|
616
|
+
startColumn: position.column - 1,
|
|
617
|
+
endLineNumber: position.lineNumber,
|
|
618
|
+
endColumn: position.column
|
|
619
|
+
},
|
|
620
|
+
text: "@"
|
|
621
|
+
}] : [];
|
|
622
|
+
const host = pinned ?? projects.find((project) => project.path === trigger.projectPath)?.host;
|
|
623
|
+
if (!host) return { suggestions };
|
|
624
|
+
const refs = await repoRefs(`https://${host}/${trigger.projectPath}`);
|
|
625
|
+
const withHost = pinned ? {
|
|
626
|
+
appendText: "",
|
|
627
|
+
edits: []
|
|
628
|
+
} : hostCompletion(model, position.lineNumber, host);
|
|
629
|
+
suggestions = refs.map((ref, index) => {
|
|
630
|
+
const inserted = completionInsert(ref.name, withHost.appendText);
|
|
631
|
+
return {
|
|
632
|
+
label: ref.kind === "commit" ? ref.name.slice(0, 7) : ref.name,
|
|
633
|
+
insertText: inserted.insertText,
|
|
634
|
+
...inserted.isSnippet ? { insertTextRules: asSnippet } : {},
|
|
635
|
+
detail: ref.detail || ref.kind,
|
|
636
|
+
sortText: String(index).padStart(4, "0"),
|
|
637
|
+
additionalTextEdits: [...slashToAtEdit, ...withHost.edits],
|
|
638
|
+
...defaultItem
|
|
639
|
+
};
|
|
640
|
+
});
|
|
641
|
+
} else if (typed === "") suggestions = [{
|
|
642
|
+
label: "Type a GitLab group or namespace",
|
|
643
|
+
insertText: "",
|
|
644
|
+
detail: "e.g. gitlab/mygroup/",
|
|
645
|
+
sortText: "0",
|
|
646
|
+
...defaultItem
|
|
647
|
+
}, ...(await repoOwners("gitlab", "", pinned)).map((owner, index) => {
|
|
648
|
+
const withHost = pinned ? {
|
|
649
|
+
appendText: "",
|
|
650
|
+
edits: []
|
|
651
|
+
} : hostCompletion(model, position.lineNumber, owner.host ?? "");
|
|
652
|
+
const ownerInsert = completionInsert(owner.name, withHost.appendText);
|
|
653
|
+
return {
|
|
654
|
+
label: owner.name,
|
|
655
|
+
filterText: owner.name,
|
|
656
|
+
insertText: ownerInsert.insertText,
|
|
657
|
+
...ownerInsert.isSnippet ? { insertTextRules: asSnippet } : {},
|
|
658
|
+
detail: pinned ? owner.kind : `${owner.host} — ${owner.kind}`,
|
|
659
|
+
additionalTextEdits: withHost.edits,
|
|
660
|
+
sortText: `1${String(index).padStart(4, "0")}`,
|
|
661
|
+
...defaultItem
|
|
662
|
+
};
|
|
663
|
+
})];
|
|
664
|
+
else if (!typed.includes("@")) suggestions = (await matchingProjects(typed, pinned)).map((project, index) => {
|
|
665
|
+
const insert = projectInsert(typed, project.path);
|
|
666
|
+
const withHost = pinned ? {
|
|
667
|
+
appendText: "",
|
|
668
|
+
edits: []
|
|
669
|
+
} : hostCompletion(model, position.lineNumber, project.host);
|
|
670
|
+
const projectInserted = completionInsert(insert.text, withHost.appendText);
|
|
671
|
+
const shown = insert.replace ? project.path : insert.text;
|
|
672
|
+
return {
|
|
673
|
+
label: project.description ? `${shown} — ${project.description}` : shown,
|
|
674
|
+
filterText: project.path,
|
|
675
|
+
insertText: projectInserted.insertText,
|
|
676
|
+
...projectInserted.isSnippet ? { insertTextRules: asSnippet } : {},
|
|
677
|
+
detail: pinned ? project.description || (project.private ? "private project" : "project") : `${project.host}${project.description ? ` — ${project.description}` : ""}`,
|
|
678
|
+
additionalTextEdits: withHost.edits,
|
|
679
|
+
range: insert.replace ? {
|
|
680
|
+
startLineNumber: position.lineNumber,
|
|
681
|
+
startColumn: gitlabIndex + 7 + 1,
|
|
682
|
+
endLineNumber: position.lineNumber,
|
|
683
|
+
endColumn: position.column
|
|
684
|
+
} : {
|
|
685
|
+
startLineNumber: position.lineNumber,
|
|
686
|
+
startColumn: position.column,
|
|
687
|
+
endLineNumber: position.lineNumber,
|
|
688
|
+
endColumn: position.column
|
|
689
|
+
},
|
|
690
|
+
sortText: `1${String(index).padStart(4, "0")}`,
|
|
691
|
+
kind: monaco.languages.CompletionItemKind.Value
|
|
692
|
+
};
|
|
693
|
+
});
|
|
322
694
|
}
|
|
323
695
|
return { suggestions };
|
|
324
696
|
}
|
|
@@ -330,76 +702,18 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
330
702
|
const lineNumber = position.lineNumber;
|
|
331
703
|
const lineContent = model.getLineContent(lineNumber);
|
|
332
704
|
const textUntilPos = lineContent.substring(0, position.column - 1);
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
kind: monaco.languages.CompletionItemKind.File,
|
|
344
|
-
range: {
|
|
345
|
-
startLineNumber: lineNumber,
|
|
346
|
-
startColumn: position.column,
|
|
347
|
-
endLineNumber: lineNumber,
|
|
348
|
-
endColumn: position.column
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
return { suggestions: (data.tree || []).filter((item) => item.type === "blob" && (item.path.endsWith(".yaml") || item.path.endsWith(".yml"))).map((f) => ({
|
|
352
|
-
label: f.path,
|
|
353
|
-
insertText: f.path,
|
|
354
|
-
detail: "yaml file",
|
|
355
|
-
...defaultItem
|
|
356
|
-
})) };
|
|
357
|
-
}
|
|
358
|
-
} catch {}
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return { suggestions: [] };
|
|
363
|
-
}
|
|
364
|
-
if (/^\s*\$thumbnail\s*:\s*$/.test(textUntilPos)) {
|
|
365
|
-
for (let ln = lineNumber - 1; ln >= Math.max(lineNumber - 30, 1); ln--) {
|
|
366
|
-
const m = model.getLineContent(ln).trim().match(/^(?:-\s+)?uses\s*:\s*github\/([^/]+)\/([^@]+)@(.+)$/);
|
|
367
|
-
if (m) {
|
|
368
|
-
const [, owner, repo, ref] = m;
|
|
369
|
-
try {
|
|
370
|
-
const resp = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${ref}?recursive=1`);
|
|
371
|
-
if (resp.ok) {
|
|
372
|
-
const data = await resp.json();
|
|
373
|
-
const defaultItem = {
|
|
374
|
-
kind: monaco.languages.CompletionItemKind.File,
|
|
375
|
-
range: {
|
|
376
|
-
startLineNumber: lineNumber,
|
|
377
|
-
startColumn: position.column,
|
|
378
|
-
endLineNumber: lineNumber,
|
|
379
|
-
endColumn: position.column
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
const imageExts = [
|
|
383
|
-
".png",
|
|
384
|
-
".jpg",
|
|
385
|
-
".jpeg",
|
|
386
|
-
".gif",
|
|
387
|
-
".svg",
|
|
388
|
-
".webp"
|
|
389
|
-
];
|
|
390
|
-
return { suggestions: (data.tree || []).filter((item) => item.type === "blob" && imageExts.some((ext) => item.path.toLowerCase().endsWith(ext))).map((f) => ({
|
|
391
|
-
label: f.path,
|
|
392
|
-
insertText: f.path,
|
|
393
|
-
detail: "image file",
|
|
394
|
-
...defaultItem
|
|
395
|
-
})) };
|
|
396
|
-
}
|
|
397
|
-
} catch {}
|
|
398
|
-
break;
|
|
399
|
-
}
|
|
705
|
+
if (fileValueKey(textUntilPos)) return { suggestions: (await fileValueSuggestions(model, lineNumber, textUntilPos, (repo, ref) => repoFiles(repo, ref, ""))).map((file) => ({
|
|
706
|
+
label: file.path,
|
|
707
|
+
insertText: file.path,
|
|
708
|
+
detail: file.detail,
|
|
709
|
+
kind: monaco.languages.CompletionItemKind.File,
|
|
710
|
+
range: {
|
|
711
|
+
startLineNumber: lineNumber,
|
|
712
|
+
startColumn: position.column,
|
|
713
|
+
endLineNumber: lineNumber,
|
|
714
|
+
endColumn: position.column
|
|
400
715
|
}
|
|
401
|
-
|
|
402
|
-
}
|
|
716
|
+
})) };
|
|
403
717
|
const isOnWithLine = /^\s*with\s*:\s*([^\n\r]*)$/.test(textUntilPos);
|
|
404
718
|
const isOnYamlLine = /^\s*\$(yaml|thumbnail)\s*:\s*\S/.test(textUntilPos);
|
|
405
719
|
let withLineNum = -1;
|
|
@@ -424,7 +738,7 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
424
738
|
if (!isOnWithLine && !isOnYamlLine && withLineNum !== -1) for (let ln = withLineNum + 1; ln < lineNumber; ln++) {
|
|
425
739
|
const content = model.getLineContent(ln).trim();
|
|
426
740
|
if (content === "") continue;
|
|
427
|
-
if (/^\$
|
|
741
|
+
if (/^\$[A-Za-z]+\s*:/.test(content)) continue;
|
|
428
742
|
return { suggestions: [] };
|
|
429
743
|
}
|
|
430
744
|
const needsNewline = isOnWithLine || isOnYamlLine;
|
|
@@ -502,24 +816,34 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
502
816
|
...defaultItem
|
|
503
817
|
}] };
|
|
504
818
|
}
|
|
505
|
-
if (!(usesValue.startsWith("marketplace/") || usesValue.startsWith("workflow/") || usesValue.startsWith("github/"))) return { suggestions: [] };
|
|
819
|
+
if (!(usesValue.startsWith("marketplace/") || usesValue.startsWith("workflow/") || usesValue.startsWith("github/") || usesValue.startsWith("gitlab/"))) return { suggestions: [] };
|
|
506
820
|
let inputs = {};
|
|
507
821
|
let usesWorkflowRef;
|
|
508
822
|
const isGithub = usesValue.startsWith("github/");
|
|
509
|
-
const
|
|
510
|
-
|
|
823
|
+
const isGitlab = usesValue.startsWith("gitlab/");
|
|
824
|
+
const isRepoRef = isGithub || isGitlab;
|
|
825
|
+
const present = needsWithLine ? specialKeysNearUses(model, usesLineNum) : specialKeysInWithBlock(model, withLineNum);
|
|
826
|
+
const offers = (key) => isRepoRef && !present.has(key);
|
|
827
|
+
if (isGitlab) {
|
|
828
|
+
const [projectPath, ref] = splitAtRef(usesValue.replace("gitlab/", ""));
|
|
829
|
+
const host = stepHostAbove(model, usesLineNum) || "gitlab.com";
|
|
830
|
+
if (!projectPath || !ref) return { suggestions: [] };
|
|
831
|
+
usesWorkflowRef = [projectPath];
|
|
832
|
+
const yamlPath = (!needsWithLine ? present.get("$yaml") : void 0) || "workflow.yaml";
|
|
833
|
+
const text = await repoFile(`https://${host}/${projectPath}`, ref, yamlPath);
|
|
834
|
+
if (text) try {
|
|
835
|
+
inputs = readExecuteInputs(yaml.load(text));
|
|
836
|
+
} catch {}
|
|
837
|
+
} else if (isGithub) {
|
|
511
838
|
const [ownerRepo, ref] = usesValue.replace("github/", "").split("@");
|
|
512
839
|
if (!ownerRepo || !ref) return { suggestions: [] };
|
|
513
840
|
const [owner, repo] = ownerRepo.split("/");
|
|
514
841
|
if (!owner || !repo) return { suggestions: [] };
|
|
515
842
|
usesWorkflowRef = [ownerRepo];
|
|
516
|
-
const yamlPath = (!needsWithLine ?
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const text = await response.text();
|
|
521
|
-
inputs = readExecuteInputs(yaml.load(text));
|
|
522
|
-
}
|
|
843
|
+
const yamlPath = (!needsWithLine ? present.get("$yaml") : void 0) || "workflow.yaml";
|
|
844
|
+
const text = await repoFile(`${owner}/${repo}`, ref ?? "", yamlPath);
|
|
845
|
+
if (text) try {
|
|
846
|
+
inputs = readExecuteInputs(yaml.load(text));
|
|
523
847
|
} catch {}
|
|
524
848
|
} else {
|
|
525
849
|
usesWorkflowRef = usesValue.split("/").slice(1);
|
|
@@ -547,15 +871,22 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
547
871
|
insertText: "",
|
|
548
872
|
...defaultItem
|
|
549
873
|
}];
|
|
550
|
-
if (
|
|
551
|
-
|
|
874
|
+
if (isGitlab && !present.has("$host")) noInputSuggestions.unshift({
|
|
875
|
+
label: "$host",
|
|
876
|
+
detail: "The registered GitLab server this project lives on (default: gitlab.com)",
|
|
877
|
+
sortText: "0b",
|
|
878
|
+
insertText: needsWithLine ? `${withIndent}with:\n${withIndent} $host:` : needsIndent ? "\n $host:" : "$host:",
|
|
879
|
+
...defaultItem
|
|
880
|
+
});
|
|
881
|
+
if (isRepoRef) {
|
|
882
|
+
if (offers("$yaml")) noInputSuggestions.unshift({
|
|
552
883
|
label: "$yaml",
|
|
553
|
-
detail: "Specify the yaml path in the
|
|
884
|
+
detail: "Specify the yaml path in the repository (default: workflow.yaml)",
|
|
554
885
|
sortText: "0",
|
|
555
886
|
insertText: needsWithLine ? `${withIndent}with:\n${withIndent} $yaml:` : needsIndent ? "\n $yaml:" : "$yaml:",
|
|
556
887
|
...defaultItem
|
|
557
888
|
});
|
|
558
|
-
noInputSuggestions.unshift({
|
|
889
|
+
if (offers("$thumbnail")) noInputSuggestions.unshift({
|
|
559
890
|
label: "$thumbnail",
|
|
560
891
|
detail: "Specify a thumbnail image path or URL for this subworkflow",
|
|
561
892
|
sortText: "0a",
|
|
@@ -657,15 +988,22 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
|
|
|
657
988
|
insertText: withSuggestionYaml,
|
|
658
989
|
...defaultItem
|
|
659
990
|
}];
|
|
660
|
-
if (
|
|
661
|
-
|
|
991
|
+
if (isGitlab && !present.has("$host")) suggestions.unshift({
|
|
992
|
+
label: "$host (GitLab server)",
|
|
993
|
+
detail: "The registered GitLab server this project lives on (default: gitlab.com)",
|
|
994
|
+
sortText: "0b",
|
|
995
|
+
insertText: needsWithLine ? `${withIndent}with:\n${withIndent} $host:` : needsIndent ? "\n $host:" : "$host:",
|
|
996
|
+
...defaultItem
|
|
997
|
+
});
|
|
998
|
+
if (isRepoRef) {
|
|
999
|
+
if (offers("$yaml")) suggestions.unshift({
|
|
662
1000
|
label: "$yaml (custom yaml path)",
|
|
663
|
-
detail: "Override the default workflow.yaml path in the
|
|
1001
|
+
detail: "Override the default workflow.yaml path in the repository",
|
|
664
1002
|
sortText: "0",
|
|
665
1003
|
insertText: needsWithLine ? `${withIndent}with:\n${withIndent} $yaml:` : needsIndent ? "\n $yaml:" : "$yaml:",
|
|
666
1004
|
...defaultItem
|
|
667
1005
|
});
|
|
668
|
-
suggestions.unshift({
|
|
1006
|
+
if (offers("$thumbnail")) suggestions.unshift({
|
|
669
1007
|
label: "$thumbnail (custom thumbnail)",
|
|
670
1008
|
detail: "Specify a thumbnail image path or URL for this subworkflow",
|
|
671
1009
|
sortText: "0a",
|
|
@@ -704,30 +1042,5 @@ function prepareWithSuggestion(needsNewline, needsIndent, needsWithLine, withInd
|
|
|
704
1042
|
else if (needsNewline) withSuggestionYaml = "\n" + withSuggestionYaml;
|
|
705
1043
|
return withSuggestionYaml;
|
|
706
1044
|
}
|
|
707
|
-
/** Scan lines below a `uses:` line for a `$yaml:` key, stopping at another step. */
|
|
708
|
-
function hasYamlNearUses(model, usesLineNumber) {
|
|
709
|
-
const totalLines = model.getLineCount();
|
|
710
|
-
for (let ln = usesLineNumber + 1; ln <= Math.min(usesLineNumber + 20, totalLines); ln++) {
|
|
711
|
-
const content = model.getLineContent(ln).trim();
|
|
712
|
-
if (content === "") continue;
|
|
713
|
-
if (/^-\s/.test(content)) break;
|
|
714
|
-
if (/^\$yaml\s*:/.test(content)) return true;
|
|
715
|
-
}
|
|
716
|
-
return false;
|
|
717
|
-
}
|
|
718
|
-
/** Scan lines below a `with:` line for a `$yaml:` key within the same indent block. */
|
|
719
|
-
function scanForYamlInWithBlock(model, withLineNumber) {
|
|
720
|
-
const withIndentMatch = model.getLineContent(withLineNumber).match(/^(\s*)/);
|
|
721
|
-
const withIndent = withIndentMatch ? withIndentMatch[1].length : 0;
|
|
722
|
-
const totalLines = model.getLineCount();
|
|
723
|
-
for (let ln = withLineNumber + 1; ln <= totalLines; ln++) {
|
|
724
|
-
const line = model.getLineContent(ln);
|
|
725
|
-
if (line.trim() === "") continue;
|
|
726
|
-
const indentMatch = line.match(/^(\s*)/);
|
|
727
|
-
if ((indentMatch ? indentMatch[1].length : 0) <= withIndent) break;
|
|
728
|
-
const yamlMatch = line.match(/^\s*\$yaml\s*:\s*(.*)$/);
|
|
729
|
-
if (yamlMatch) return yamlMatch[1].trim();
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
1045
|
//#endregion
|
|
733
1046
|
export { MonacoEditor as default };
|