Skip to content
Draft
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
48 changes: 44 additions & 4 deletions packages/darklang/cli/core.dark
Original file line number Diff line number Diff line change
Expand Up @@ -533,24 +533,64 @@ let runInteractiveLoop (state: AppState) : Int64 =
let inputStartColumn =
Prompt.Display.calculateInputStartColumn locationStr

// Calculate total line length to detect if it exceeds terminal width
let hint = Registry.getCompletionHint state state.prompt.text
let totalLineLength =
inputStartColumn + (Stdlib.String.length state.prompt.text) + (Stdlib.String.length hint)
let terminalWidth = Terminal.getWidth ()
let lineExceedsWidth = totalLineLength > terminalWidth

if state.needsFullRedraw then
// Redraw entire prompt line (used after commands that change state)
// Full redraw (used after commands that change state)
let promptText = View.formatPromptWithInput state
Stdlib.print promptText
else if lineExceedsWidth then
// Long line update: overwrite content without clearing first to reduce flicker
Stdlib.print "\u001b[?25l" // Hide cursor during redraw

// Calculate which row the cursor was on based on previous text length
let previousTotalLength = inputStartColumn + state.prompt.previousTextLength
let previousLineWrapped = previousTotalLength > terminalWidth

if previousLineWrapped then
// Cursor was on a wrapped line, move up to prompt start
let wrappedLines = Stdlib.Int64.divide (previousTotalLength - 1L) terminalWidth
if wrappedLines > 0L then
Stdlib.print (Colors.moveCursorUp wrappedLines)

// Go to start of line and overwrite with new content
Stdlib.print Colors.carriageReturn
let promptText = View.formatPromptWithInput state
Stdlib.print promptText
// Clear to end of line in case new text is shorter than old
Stdlib.print Colors.clearLine
Stdlib.print "\u001b[?25h" // Show cursor again
else
// Update only the input portion (used during typing)
// Short line incremental update (used during typing when line fits)
Stdlib.print "\u001b[?25l" // Hide cursor during redraw
Stdlib.print (Colors.moveCursorToColumn inputStartColumn)
Stdlib.print Colors.clearLine
Stdlib.print state.prompt.text
let hint = Registry.getCompletionHint state state.prompt.text
if Stdlib.Bool.not (Stdlib.String.isEmpty hint) then
Stdlib.print (Colors.hint hint)
Stdlib.print "\u001b[?25h" // Show cursor again

// Position cursor at correct location within user input
let cursorColumn = inputStartColumn + state.prompt.cursorPosition
Stdlib.print (Colors.moveCursorToColumn cursorColumn)
if lineExceedsWidth then
// For wrapped lines, calculate which row and column the cursor should be on
let absoluteCursorPos = inputStartColumn + state.prompt.cursorPosition
let cursorRow = Stdlib.Int64.divide (absoluteCursorPos - 1L) terminalWidth
let cursorCol = ((absoluteCursorPos - 1L) % terminalWidth) + 1L
// After reprinting, cursor is at end of text - calculate where it should go
let textEndRow = Stdlib.Int64.divide (totalLineLength - 1L) terminalWidth
// Move up from text end to cursor row if needed
let rowDiff = textEndRow - cursorRow
if rowDiff > 0L then
Stdlib.print (Colors.moveCursorUp rowDiff)
Stdlib.print (Colors.moveCursorToColumn cursorCol)
else
Stdlib.print (Colors.moveCursorToColumn cursorColumn)
| InteractiveNav navState ->
// Always display in interactive nav mode - it manages its own screen clearing
Packages.NavInteractive.display navState
Expand Down
36 changes: 26 additions & 10 deletions packages/darklang/cli/prompt.dark
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,46 @@ type State =
cursorPosition: Int64
commandHistory: List<String>
historyIndex: Int64
savedPrompt: String }
savedPrompt: String
/// Tracks text length from last display update, used to calculate cursor row for long lines
previousTextLength: Int64 }

let initState () : State =
State
{ text = ""
cursorPosition = 0L
commandHistory = []
historyIndex = -1L
savedPrompt = "" }
savedPrompt = ""
previousTextLength = 0L }


module Editing =
/// Insert text at cursor position
let insertAtCursor (state: State) (char: String) : State =
let prevLen = Stdlib.String.length state.text
let beforeCursor = Stdlib.String.slice state.text 0L state.cursorPosition
let afterCursor = Stdlib.String.dropFirst state.text state.cursorPosition
let newText = beforeCursor ++ char ++ afterCursor
{ state with
text = newText
cursorPosition = state.cursorPosition + (Stdlib.String.length char)
historyIndex = -1L
savedPrompt = "" }
savedPrompt = ""
previousTextLength = prevLen }

/// Delete character before cursor (backspace)
let deleteBeforeCursor (state: State) : State =
if state.cursorPosition > 0L then
let prevLen = Stdlib.String.length state.text
let beforeCursor = Stdlib.String.slice state.text 0L (state.cursorPosition - 1L)
let afterCursor = Stdlib.String.dropFirst state.text state.cursorPosition
let newText = beforeCursor ++ afterCursor
{ state with
text = newText
cursorPosition = state.cursorPosition - 1L
historyIndex = -1L }
historyIndex = -1L
previousTextLength = prevLen }
else
state

Expand All @@ -47,10 +54,11 @@ module Editing =
let textLength = Stdlib.String.length state.text

if state.cursorPosition < textLength then
let prevLen = textLength
let beforeCursor = Stdlib.String.slice state.text 0L state.cursorPosition
let afterCursor = Stdlib.String.dropFirst state.text (state.cursorPosition + 1L)
let newText = beforeCursor ++ afterCursor
{ state with text = newText; historyIndex = -1L }
{ state with text = newText; historyIndex = -1L; previousTextLength = prevLen }
else
state

Expand All @@ -76,15 +84,18 @@ module Editing =

/// Clear the prompt text
let clear (state: State) : State =
let prevLen = Stdlib.String.length state.text
{ state with
text = ""
cursorPosition = 0L
historyIndex = -1L }
historyIndex = -1L
previousTextLength = prevLen }

/// Set the prompt text and move cursor to end
let setText (state: State) (text: String) : State =
let prevLen = Stdlib.String.length state.text
let textLength = Stdlib.String.length text
{ state with text = text; cursorPosition = textLength }
{ state with text = text; cursorPosition = textLength; previousTextLength = prevLen }


module History =
Expand All @@ -102,6 +113,7 @@ module History =
/// Navigate to previous command in history
let navigatePrevious (state: State) : State =
if Stdlib.Bool.not (Stdlib.List.isEmpty state.commandHistory) then
let prevLen = Stdlib.String.length state.text
let historyLength = Stdlib.List.length state.commandHistory
let newIndex =
if state.historyIndex == -1L then
Expand All @@ -123,7 +135,8 @@ module History =
text = command
cursorPosition = commandLength
historyIndex = newIndex
savedPrompt = newSavedPrompt }
savedPrompt = newSavedPrompt
previousTextLength = prevLen }
| None ->
state
else
Expand All @@ -132,6 +145,7 @@ module History =
/// Navigate to next command in history
let navigateNext (state: State) : State =
if Stdlib.Bool.not (Stdlib.List.isEmpty state.commandHistory) && state.historyIndex >= 0L then
let prevLen = Stdlib.String.length state.text
let newIndex =
if state.historyIndex > 0L then
state.historyIndex - 1L
Expand All @@ -142,15 +156,17 @@ module History =
{ state with
text = state.savedPrompt
cursorPosition = restoredPromptLength
historyIndex = newIndex }
historyIndex = newIndex
previousTextLength = prevLen }
else
match Stdlib.List.getAt state.commandHistory newIndex with
| Some command ->
let commandLength = Stdlib.String.length command
{ state with
text = command
cursorPosition = commandLength
historyIndex = newIndex }
historyIndex = newIndex
previousTextLength = prevLen }
| None ->
state
else
Expand Down
1 change: 1 addition & 0 deletions packages/darklang/cli/utils/colors.dark
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ let underline = "\u001b[4m"
let clearLine = "\u001b[K"
let carriageReturn = "\r"
let moveCursorToColumn (col: Int64) : String = $"\u001b[{Stdlib.Int64.toString col}G"
let moveCursorUp (n: Int64) : String = $"\u001b[{Stdlib.Int64.toString n}A"
let saveCursor = "\u001b[s"
let restoreCursor = "\u001b[u"
/// Move cursor to absolute position (row, col). Used to jump to the status bar without disrupting where the user is typing.
Expand Down