Skip to content

generate: make interface consistent with sync and fmt #619

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 28, 2024
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ Options for fmt:
-u, --update Prompt to write formatted files
-y, --yes Auto-confirm the prompt from --update

Options for generate:
-e, --exercise <slug> Only operate on this exercise
-u, --update Prompt to write generated files
-y, --yes Auto-confirm the prompt from --update

Options for info:
-o, --offline Do not update the cached 'problem-specifications' data

Expand Down
9 changes: 8 additions & 1 deletion completions/configlet.bash
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,14 @@ _configlet_complete_lint_() {
}

_configlet_complete_generate_() {
_configlet_complete_options_ "$global_opts"
case $prev in
'-e' | '--exercise')
_configlet_complete_slugs_ "practice" "concept"
;;
*)
_configlet_complete_options_ "-e --exercise -u --update -y --yes $global_opts"
;;
esac
}

_configlet_complete_info_() {
Expand Down
7 changes: 6 additions & 1 deletion completions/configlet.fish
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ end
complete -c configlet -f

# subcommands with no options
complete -c configlet -n "__fish_use_subcommand" -a generate -d "Generate concept exercise introductions"
complete -c configlet -n "__fish_use_subcommand" -a lint -d "Check the track configuration for correctness"

# subcommands with options
Expand Down Expand Up @@ -37,6 +36,12 @@ complete -c configlet -n "__fish_seen_subcommand_from fmt" -s e -l exerci
complete -c configlet -n "__fish_seen_subcommand_from fmt" -s u -l update -d "Write changes"
complete -c configlet -n "__fish_seen_subcommand_from fmt" -s y -l yes -d "Auto-confirm update"

# generate subcommand
complete -c configlet -n "__fish_seen_subcommand_from generate" -s e -l exercise -d "exercise slug" \
-xa '(__fish_configlet_find_dirs ./exercises/{concept,practice})'
complete -c configlet -n "__fish_seen_subcommand_from generate" -s u -l update -d "Write changes"
complete -c configlet -n "__fish_seen_subcommand_from generate" -s y -l yes -d "Auto-confirm update"

# info subcommand
complete -c configlet -n "__fish_seen_subcommand_from info" -s o -l offline -d "Do not update prob-specs cache"

Expand Down
3 changes: 3 additions & 0 deletions completions/configlet.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ _configlet() {
(generate)
_arguments "${_arguments_options[@]}" \
"$_configlet_global_opts[@]" \
'(-e --exercise)'{-e+,--exercise=}'[exercise slug]:slug:_configlet_complete_any_exercise_slug' \
{-u,--update}'[Write changes]' \
{-y,--yes}'[Auto-confirm update]' \
;;
(lint)
_arguments "${_arguments_options[@]}" \
Expand Down
78 changes: 48 additions & 30 deletions src/cli.nim
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type

Action* = object
case kind*: ActionKind
of actNil, actGenerate, actLint:
of actNil, actLint:
discard
of actCompletion:
shell*: Shell
Expand All @@ -56,6 +56,10 @@ type
exerciseFmt*: string
updateFmt*: bool
yesFmt*: bool
of actGenerate:
exerciseGenerate*: string
updateGenerate*: bool
yesGenerate*: bool
of actInfo:
offlineInfo*: bool
of actSync:
Expand Down Expand Up @@ -96,12 +100,12 @@ type
# Options for `completion`
optCompletionShell = "shell"

# Options for `create`, `fmt` and `sync`
optFmtSyncCreateExercise = "exercise"
# Options for `create`, `fmt`, generate, and `sync`
optFmtGenerateSyncCreateExercise = "exercise"

# Options for both `fmt` and `sync`
optFmtSyncUpdate = "update"
optFmtSyncYes = "yes"
# Options for `fmt`, `generate`, and `sync`
optFmtGenerateSyncUpdate = "update"
optFmtGenerateSyncYes = "yes"

# Options for both `info`, `sync` and `create`
optInfoSyncCreateOffline = "offline"
Expand All @@ -128,7 +132,7 @@ func genShortKeys: array[Opt, char] =
const
configletVersion = staticRead("../configlet.version").strip()
short = genShortKeys()
optsNoVal = {optHelp, optVersion, optFmtSyncUpdate, optFmtSyncYes,
optsNoVal = {optHelp, optVersion, optFmtGenerateSyncUpdate, optFmtGenerateSyncYes,
optInfoSyncCreateOffline, optSyncDocs, optSyncFilepaths, optSyncMetadata}

func generateNoVals: tuple[shortNoVal: set[char], longNoVal: seq[string]] =
Expand Down Expand Up @@ -186,7 +190,7 @@ func genHelpText: string =
of optTrackDir: "dir"
of optVerbosity: "verbosity"
of optCompletionShell: "shell"
of optFmtSyncCreateExercise: "slug"
of optFmtGenerateSyncCreateExercise: "slug"
of optCreateApproach: "slug"
of optCreateArticle: "slug"
of optCreateConceptExercise: "slug"
Expand Down Expand Up @@ -252,15 +256,15 @@ func genHelpText: string =
optCreateDifficulty: "The difficulty of the exercise (default: 1)",
optCompletionShell: &"Choose the shell type (required)\n" &
&"{paddingOpt}{allowedValues(Shell)}",
optFmtSyncCreateExercise: "Only operate on this exercise",
optFmtSyncUpdate: "Prompt to update the unsynced track data",
optFmtSyncYes: &"Auto-confirm prompts from --{$optFmtSyncUpdate} for updating docs, filepaths, and metadata",
optFmtGenerateSyncCreateExercise: "Only operate on this exercise",
optFmtGenerateSyncUpdate: "Prompt to update the unsynced track data",
optFmtGenerateSyncYes: &"Auto-confirm prompts from --{$optFmtGenerateSyncUpdate} for updating docs, filepaths, and metadata",
optInfoSyncCreateOffline: "Do not update the cached 'problem-specifications' data",
optSyncDocs: "Sync Practice Exercise '.docs/introduction.md' and '.docs/instructions.md' files",
optSyncFilepaths: "Populate empty 'files' values in Concept/Practice exercise '.meta/config.json' files",
optSyncMetadata: "Sync Practice Exercise '.meta/config.json' metadata values",
optSyncTests: &"Sync Practice Exercise '.meta/tests.toml' files.\n" &
&"{paddingOpt}The mode value specifies how missing tests are handled when using --{$optFmtSyncUpdate}.\n" &
&"{paddingOpt}The mode value specifies how missing tests are handled when using --{$optFmtGenerateSyncUpdate}.\n" &
&"{paddingOpt}{allowedValues(TestsMode)} (default: choose)",
optUuidNum: "Number of UUIDs to output",
]
Expand Down Expand Up @@ -312,13 +316,13 @@ func genHelpText: string =
of "practiceExerciseSlug":
optCreatePracticeExercise
of "exerciseCreate":
optFmtSyncCreateExercise
of "exerciseFmt":
optFmtSyncCreateExercise
of "updateFmt":
optFmtSyncUpdate
of "yesFmt":
optFmtSyncYes
optFmtGenerateSyncCreateExercise
of "exerciseFmt", "exerciseGenerate":
optFmtGenerateSyncCreateExercise
of "updateFmt", "updateGenerate":
optFmtGenerateSyncUpdate
of "yesFmt", "yesGenerate":
optFmtGenerateSyncYes
of "offlineInfo":
optInfoSyncCreateOffline
of "offlineCreate":
Expand All @@ -327,10 +331,14 @@ func genHelpText: string =
parseEnum[Opt](key)
# Set the description for `fmt` options.
let desc =
if actionKind == actFmt and opt == optFmtSyncUpdate:
if actionKind == actFmt and opt == optFmtGenerateSyncUpdate:
"Prompt to write formatted files"
elif actionKind == actFmt and opt == optFmtSyncYes:
&"Auto-confirm the prompt from --{$optFmtSyncUpdate}"
elif actionKind == actFmt and opt == optFmtGenerateSyncYes:
&"Auto-confirm the prompt from --{$optFmtGenerateSyncUpdate}"
elif actionKind == actGenerate and opt == optFmtGenerateSyncUpdate:
"Prompt to write generated files"
elif actionKind == actGenerate and opt == optFmtGenerateSyncYes:
&"Auto-confirm the prompt from --{$optFmtGenerateSyncUpdate}"
else:
optionDescriptions[opt]
result.add alignLeft(syntax[opt], maxLen) & desc & "\n"
Expand Down Expand Up @@ -536,7 +544,7 @@ proc handleOption(conf: var Conf; kind: CmdLineKind; key, val: string) =
# Process action-specific options
if not isGlobalOpt:
case conf.action.kind
of actNil, actGenerate, actLint:
of actNil, actLint:
discard
of actCompletion:
case opt
Expand All @@ -550,7 +558,7 @@ proc handleOption(conf: var Conf; kind: CmdLineKind; key, val: string) =
setActionOpt(approachSlug, val)
of optCreateArticle:
setActionOpt(articleSlug, val)
of optFmtSyncCreateExercise:
of optFmtGenerateSyncCreateExercise:
setActionOpt(exerciseCreate, val)
of optCreateConceptExercise:
setActionOpt(conceptExerciseSlug, val)
Expand All @@ -571,14 +579,24 @@ proc handleOption(conf: var Conf; kind: CmdLineKind; key, val: string) =
discard
of actFmt:
case opt
of optFmtSyncCreateExercise:
of optFmtGenerateSyncCreateExercise:
setActionOpt(exerciseFmt, val)
of optFmtSyncUpdate:
of optFmtGenerateSyncUpdate:
setActionOpt(updateFmt, true)
of optFmtSyncYes:
of optFmtGenerateSyncYes:
setActionOpt(yesFmt, true)
else:
discard
of actGenerate:
case opt
of optFmtGenerateSyncCreateExercise:
setActionOpt(exerciseGenerate, val)
of optFmtGenerateSyncUpdate:
setActionOpt(updateGenerate, true)
of optFmtGenerateSyncYes:
setActionOpt(yesGenerate, true)
else:
discard
of actInfo:
case opt
of optInfoSyncCreateOffline:
Expand All @@ -587,11 +605,11 @@ proc handleOption(conf: var Conf; kind: CmdLineKind; key, val: string) =
discard
of actSync:
case opt
of optFmtSyncCreateExercise:
of optFmtGenerateSyncCreateExercise:
setActionOpt(exercise, val)
of optFmtSyncUpdate:
of optFmtGenerateSyncUpdate:
setActionOpt(update, true)
of optFmtSyncYes:
of optFmtGenerateSyncYes:
setActionOpt(yes, true)
of optSyncTests:
setActionOpt(tests, parseVal[TestsMode](kind, key, val))
Expand Down
90 changes: 77 additions & 13 deletions src/generate/generate.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import std/[parseutils, strbasics, strformat, strscans, strutils, sugar, tables,
terminal]
import ".."/[cli, helpers, types_track_config]
import std/[os, parseutils, strbasics, strformat, strscans, strutils, sugar,
tables, terminal]
import ".."/[cli, helpers, logger, types_track_config]

type
PathAndGeneratedDocument = object
path: string
generatedDocument: string

proc getConceptSlugLookup(trackDir: Path): Table[Slug, string] =
## Returns a `Table` that maps each concept's `slug` to its `name`.
Expand Down Expand Up @@ -133,18 +138,77 @@ proc generateIntroduction(trackDir: Path, templatePath: Path,
result.add linkDef
result.add '\n'

iterator getIntroductionTemplatePaths(trackDir: Path, conf: Conf): Path =
let conceptExercisesDir = trackDir / "exercises" / "concept"
if dirExists(conceptExercisesDir):
for conceptExerciseDir in getSortedSubdirs(conceptExercisesDir):
if conf.action.exerciseGenerate.len == 0 or conf.action.exerciseGenerate == $conceptExerciseDir.splitFile.name:
let introductionTemplatePath = conceptExerciseDir / ".docs" / "introduction.md.tpl"
if fileExists(introductionTemplatePath):
yield introductionTemplatePath

proc generateImpl(trackDir: Path, conf: Conf): seq[PathAndGeneratedDocument] =
result = @[]

let slugLookup = getConceptSlugLookup(trackDir)

for introductionTemplatePath in getIntroductionTemplatePaths(trackDir, conf):
let generated = generateIntroduction(trackDir, introductionTemplatePath,
slugLookup)
let introductionPath = introductionTemplatePath.string[0..^5] # Removes `.tpl`

if fileExists(introductionPath) and readFile(introductionPath) == generated:
logDetailed(&"Up-to-date: {relativePath(introductionPath, $trackDir)}")
else:
logNormal(&"Outdated: {relativePath(introductionPath, $trackDir)}")
result.add PathAndGeneratedDocument(
path: introductionPath,
generatedDocument: generated
)

proc writeGenerated(generatedPairs: seq[PathAndGeneratedDocument]) =
for generatedPair in generatedPairs:
let path = generatedPair.path
doAssert lastPathPart(path) == "introduction.md"
createDir path.parentDir()
logDetailed(&"Generating: {path}")
writeFile(path, generatedPair.generatedDocument)
let s = if generatedPairs.len > 1: "s" else: ""
logNormal(&"Generated {generatedPairs.len} file{s}")

proc userSaysYes(userExercise: string): bool =
## Asks the user if they want to format files, and returns `true` if they
## confirm.
let s = if userExercise.len > 0: "" else: "s"
while true:
stderr.write &"Generate (update) the above file{s} ([y]es/[n]o)? "
case stdin.readLine().toLowerAscii()
of "y", "yes":
return true
of "n", "no":
return false
else:
stderr.writeLine "Unrecognized response. Please answer [y]es or [n]o."

proc generate*(conf: Conf) =
## For every Concept Exercise in `conf.trackDir` with an `introduction.md.tpl`
## file, write the corresponding `introduction.md` file.
let trackDir = Path(conf.trackDir)
let pairs = generateImpl(trackDir, conf)

let conceptExercisesDir = trackDir / "exercises" / "concept"
if dirExists(conceptExercisesDir):
let slugLookup = getConceptSlugLookup(trackDir)
for conceptExerciseDir in getSortedSubdirs(conceptExercisesDir):
let introductionTemplatePath = conceptExerciseDir / ".docs" / "introduction.md.tpl"
if fileExists(introductionTemplatePath):
let introduction = generateIntroduction(trackDir, introductionTemplatePath,
slugLookup)
let introductionPath = introductionTemplatePath.string[0..^5] # Removes `.tpl`
writeFile(introductionPath, introduction)
let userExercise = conf.action.exerciseGenerate
if pairs.len > 0:
if conf.action.updateGenerate:
if conf.action.yesGenerate or userSaysYes(userExercise):
writeGenerated(pairs)
else:
quit QuitFailure
else:
quit QuitFailure
else:
let wording =
if userExercise.len > 0:
&"The `{userExercise}`"
else:
"Every"
logNormal(&"{wording} introduction file is up-to-date!")
1 change: 1 addition & 0 deletions src/helpers.nim
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ proc fileExists*(path: Path): bool {.borrow.}
proc readFile*(path: Path): string {.borrow.}
proc writeFile*(path: Path; content: string) {.borrow.}
proc parentDir*(path: Path): string {.borrow.}
proc splitFile*(path: Path): tuple[dir, name: Path, ext: string] {.borrow.}

func toLineAndCol(s: string; offset: Natural): tuple[line: int; col: int] =
## Returns the line and column number corresponding to the `offset` in `s`.
Expand Down
Loading