1package main
  2
  3// Colon command handler (e.g., :q, :w, :help). It processes strings entered in
  4// ModeCommand and executes the corresponding actions.
  5
  6import (
  7	"bufio"
  8	"fmt"
  9	"os"
 10	"os/exec"
 11	"strconv"
 12	"strings"
 13
 14	"github.com/nsf/termbox-go"
 15)
 16
 17// Command provides a context for executing editor commands.
 18type Command struct {
 19	e *Editor
 20}
 21
 22// IsValidCommand returns true if the command should be saved to history.
 23// Line numbers (pure integers) are not saved to history.
 24func (ch *Command) IsValidCommand(cmd string) bool {
 25	cmd = strings.TrimSpace(cmd)
 26	if cmd == "" {
 27		return false
 28	}
 29
 30	// Check if it's a pure number (line jump command) - don't save to history
 31	if _, err := strconv.Atoi(cmd); err == nil {
 32		return false
 33	}
 34
 35	// Valid if it's a known command
 36	switch cmd {
 37	case "q", "Q", "q!", "Q!", "w", "W", "wa", "WA", "wq", "WQ", "waq", "WAQ", "reload", "bd", "bd!", "debug", "help", "mouse", "e", "edit", "n":
 38		return true
 39	}
 40
 41	// Valid if it starts with ! (shell command) or r! (read shell)
 42	if strings.HasPrefix(cmd, "!") || strings.HasPrefix(cmd, "r!") {
 43		return true
 44	}
 45
 46	// Valid if it starts with w (write with filename)
 47	if strings.HasPrefix(cmd, "w ") {
 48		return true
 49	}
 50
 51	// Everything else is considered invalid (will show "Command not found" message)
 52	return false
 53}
 54
 55// HandleAndSaveToHistory executes a command and saves it to history if valid.
 56func (ch *Command) HandleAndSaveToHistory(cmd string) {
 57	ch.Handle(cmd)
 58	// Save to history only if it's a valid command (not a line number, not unrecognized)
 59	if ch.IsValidCommand(cmd) {
 60		if len(ch.e.commandHistory) == 0 || ch.e.commandHistory[len(ch.e.commandHistory)-1] != cmd {
 61			ch.e.commandHistory = append(ch.e.commandHistory, cmd)
 62		}
 63	}
 64}
 65
 66// NavigateHistoryUp moves backward through command history.
 67func (ch *Command) NavigateHistoryUp() {
 68	if len(ch.e.commandHistory) == 0 {
 69		return
 70	}
 71
 72	if ch.e.commandHistoryIdx == -1 {
 73		// Starting navigation from the end
 74		ch.e.commandHistoryIdx = len(ch.e.commandHistory) - 1
 75	} else if ch.e.commandHistoryIdx > 0 {
 76		ch.e.commandHistoryIdx--
 77	}
 78
 79	if ch.e.commandHistoryIdx >= 0 && ch.e.commandHistoryIdx < len(ch.e.commandHistory) {
 80		ch.e.commandBuffer = []rune(ch.e.commandHistory[ch.e.commandHistoryIdx])
 81		ch.e.commandCursorX = len(ch.e.commandBuffer)
 82	}
 83}
 84
 85// NavigateHistoryDown moves forward through command history.
 86func (ch *Command) NavigateHistoryDown() {
 87	if ch.e.commandHistoryIdx == -1 {
 88		return
 89	}
 90
 91	ch.e.commandHistoryIdx++
 92	if ch.e.commandHistoryIdx >= len(ch.e.commandHistory) {
 93		// Reached the end, clear the buffer
 94		ch.e.commandHistoryIdx = -1
 95		ch.e.commandBuffer = []rune{}
 96		ch.e.commandCursorX = 0
 97	} else {
 98		ch.e.commandBuffer = []rune(ch.e.commandHistory[ch.e.commandHistoryIdx])
 99		ch.e.commandCursorX = len(ch.e.commandBuffer)
100	}
101}
102
103// Handle parses and executes a command string.
104func (ch *Command) Handle(cmd string) {
105	cmd = strings.TrimSpace(cmd)
106	switch {
107	case cmd == "q" || cmd == "Q":
108		ch.quit(false)
109	case cmd == "q!" || cmd == "Q!":
110		ch.quit(true)
111	case cmd == "w" || cmd == "W":
112		ch.write("")
113	case strings.HasPrefix(cmd, "w "):
114		filename := strings.TrimSpace(strings.TrimPrefix(cmd, "w "))
115		ch.write(filename)
116	case cmd == "wa" || cmd == "WA":
117		ch.writeAll()
118	case cmd == "wq" || cmd == "WQ":
119		ch.writeQuit()
120	case cmd == "waq" || cmd == "WAQ":
121		ch.writeAll()
122		// Check if any buffers are still modified (meaning save failed)
123		hasModified := false
124		for _, b := range ch.e.buffers {
125			if b.modified && b.filename != "" && !b.readOnly {
126				hasModified = true
127				break
128			}
129		}
130		// Only quit if all files were saved successfully
131		if !hasModified {
132			ch.quit(false)
133		}
134	case cmd == "reload":
135		ch.reload()
136	case cmd == "bd":
137		ch.bufferDelete(false)
138	case cmd == "bd!":
139		ch.bufferDelete(true)
140	case cmd == "n":
141		ch.e.NewBuffer()
142	case cmd == "debug":
143		ch.e.toggleDebugWindow()
144	case cmd == "help":
145		// Load help content from the embedded filesystem.
146		f, err := ContentFS.Open("content/help.txt")
147		if err != nil {
148			ch.e.message = fmt.Sprintf("Error opening help: %v", err)
149		} else {
150			defer f.Close()
151			err = ch.e.LoadFromReader("help.txt", f)
152			if err != nil {
153				ch.e.message = fmt.Sprintf("Error loading help: %v", err)
154			} else {
155				// Help is read-only to prevent accidental edits.
156				b := ch.e.activeBuffer()
157				if b != nil {
158					b.readOnly = true
159					ch.e.message = "Help opened (Read-Only)"
160				}
161			}
162		}
163	case cmd == "mouse":
164		ch.toggleMouse()
165	case strings.HasPrefix(cmd, "e ") || strings.HasPrefix(cmd, "edit "):
166		filename := ""
167		if strings.HasPrefix(cmd, "e ") {
168			filename = strings.TrimSpace(strings.TrimPrefix(cmd, "e "))
169		} else {
170			filename = strings.TrimSpace(strings.TrimPrefix(cmd, "edit "))
171		}
172		if filename != "" {
173			err := ch.e.LoadFile(filename)
174			if err != nil {
175				ch.e.message = fmt.Sprintf("Error opening file: %v", err)
176			} else {
177				ch.e.message = fmt.Sprintf("Opened: %s", filename)
178			}
179		} else {
180			ch.e.message = "No filename specified"
181		}
182	case cmd == "e" || cmd == "edit":
183		ch.e.message = "No filename specified"
184	default:
185		if cmd == "" {
186			break
187		}
188		// If the command starts with r!, execute it and insert output into buffer.
189		if strings.HasPrefix(cmd, "r!") {
190			shellCmd := strings.TrimPrefix(cmd, "r!")
191			ch.readShell(shellCmd)
192			break
193		}
194		// If the command starts with !, execute it as a shell command.
195		if strings.HasPrefix(cmd, "!") {
196			shellCmd := strings.TrimPrefix(cmd, "!")
197			ch.executeShell(shellCmd)
198			break
199		}
200		// If the command is a number, jump to that line.
201		if lineNum, err := strconv.Atoi(cmd); err == nil {
202			ch.goToLine(lineNum)
203		} else {
204			ch.e.message = fmt.Sprintf("Command not found: %s", cmd)
205		}
206	}
207	// After executing a command, return to Normal mode and clear the command buffer.
208	if ch.e.mode == ModeCommand {
209		ch.e.mode = ModeNormal
210	}
211	ch.e.commandBuffer = []rune{}
212}
213
214// quit exits the editor, checking for unsaved changes unless 'force' is true.
215func (ch *Command) quit(force bool) {
216	if !force {
217		// Check if any buffer has unsaved changes
218		for _, b := range ch.e.buffers {
219			if b.modified {
220				ch.e.message = "No write since last change (use :q! to override)"
221				return
222			}
223		}
224	}
225	termbox.Close()
226	os.Exit(0)
227}
228
229// write saves the current active buffer to disk.
230func (ch *Command) write(filename string) {
231	if filename != "" {
232		b := ch.e.activeBuffer()
233		if b != nil {
234			b.filename = filename
235			b.fileType = getFileType(filename)
236		}
237	}
238	err := ch.e.SaveFile(false)
239	if err != nil {
240		// Handle conflict if the file was changed externally.
241		if err.Error() == "file changed on disk" {
242			ch.e.message = "File changed on disk. Overwrite? (y/n) "
243			ch.e.mode = ModeConfirm
244			ch.e.pendingConfirm = func() {
245				err := ch.e.SaveFile(true) // Force overwrite.
246				if err != nil {
247					ch.e.message = err.Error()
248				} else {
249					name := ch.e.activeBuffer().filename
250					if name == "" {
251						name = "[No Name]"
252					}
253					ch.e.message = fmt.Sprintf("\"%s\" written", name)
254				}
255			}
256		} else {
257			ch.e.message = err.Error()
258		}
259	} else {
260		name := ch.e.activeBuffer().filename
261		if name == "" {
262			name = "[No Name]"
263		}
264		ch.e.message = fmt.Sprintf("\"%s\" written", name)
265	}
266}
267
268// writeQuit saves the current buffer and exits.
269func (ch *Command) writeQuit() {
270	err := ch.e.SaveFile(false)
271	if err != nil {
272		if err.Error() == "file changed on disk" {
273			ch.e.message = "File changed on disk. Overwrite? (y/n) "
274			ch.e.mode = ModeConfirm
275			ch.e.pendingConfirm = func() {
276				err := ch.e.SaveFile(true)
277				if err == nil {
278					termbox.Close()
279					os.Exit(0)
280				} else {
281					ch.e.message = err.Error()
282				}
283			}
284		} else {
285			ch.e.message = err.Error()
286		}
287	} else {
288		termbox.Close()
289		os.Exit(0)
290	}
291}
292
293// writeAll saves all open buffers to disk.
294func (ch *Command) writeAll() {
295	savedCount := 0
296	var lastErr error
297
298	// Iterate through all buffers and save each one.
299	for _, b := range ch.e.buffers {
300		// Skip buffers without filenames (e.g., [No Name] buffers).
301		if b.filename == "" {
302			continue
303		}
304
305		// Skip read-only buffers.
306		if b.readOnly {
307			continue
308		}
309
310		// Save the buffer using the same logic as SaveFile but for each buffer.
311		file, err := os.Create(b.filename)
312		if err != nil {
313			lastErr = err
314			continue
315		}
316
317		writer := bufio.NewWriter(file)
318		for i, line := range b.buffer {
319			_, err := writer.WriteString(string(line))
320			if err != nil {
321				file.Close()
322				lastErr = err
323				continue
324			}
325			// Write newline if not the last line (or if buffer should end with newline).
326			if i < len(b.buffer)-1 || (len(b.buffer) > 0 && (len(b.buffer) > 1 || len(b.buffer[0]) > 0)) {
327				_, err = writer.WriteString("\n")
328				if err != nil {
329					file.Close()
330					lastErr = err
331					continue
332				}
333			}
334		}
335
336		err = writer.Flush()
337		file.Close()
338
339		if err == nil {
340			b.modified = false
341			info, err := os.Stat(b.filename)
342			if err == nil {
343				b.lastModTime = info.ModTime()
344			}
345			savedCount++
346		} else {
347			lastErr = err
348		}
349	}
350
351	// Display appropriate message.
352	if lastErr != nil {
353		ch.e.message = fmt.Sprintf("Error saving some files: %v", lastErr)
354	} else if savedCount == 0 {
355		ch.e.message = "No files to save"
356	} else if savedCount == 1 {
357		ch.e.message = "1 file written"
358	} else {
359		ch.e.message = fmt.Sprintf("%d files written", savedCount)
360	}
361}
362
363// bufferDelete closes the currently active buffer.
364func (ch *Command) bufferDelete(force bool) {
365	b := ch.e.activeBuffer()
366	if !force && b != nil && b.modified {
367		ch.e.message = "No write since last change (use :bd! to override)"
368		return
369	}
370	ch.e.deleteCurrentBuffer()
371}
372
373// toggleMouse enables/disables mouse interaction in the terminal.
374func (ch *Command) toggleMouse() {
375	ch.e.mouseEnabled = !ch.e.mouseEnabled
376	if ch.e.mouseEnabled {
377		termbox.SetInputMode(termbox.InputEsc | termbox.InputMouse)
378	} else {
379		termbox.SetInputMode(termbox.InputEsc)
380	}
381}
382
383// goToLine moves the cursor to the beginning of the specified line number.
384func (ch *Command) goToLine(lineNum int) {
385	b := ch.e.activeBuffer()
386	if b != nil {
387		targetY := lineNum - 1 // Convert 1-based UI line number to 0-based index.
388		if targetY < 0 {
389			targetY = 0
390		}
391		if targetY >= len(b.buffer) {
392			targetY = len(b.buffer) - 1
393		}
394		b.PrimaryCursor().Y = targetY
395		b.PrimaryCursor().X = 0
396		ch.e.centerCursor()
397	}
398}
399
400// reload re-reads the active buffer from disk.
401func (ch *Command) reload() {
402	b := ch.e.activeBuffer()
403	if b != nil {
404		err := ch.e.ReloadBuffer(b)
405		if err != nil {
406			ch.e.message = fmt.Sprintf("Reload failed: %v", err)
407		} else {
408			ch.e.message = fmt.Sprintf("\"%s\" reloaded", b.filename)
409		}
410	}
411}
412
413// executeShell runs a shell command and displays the output.
414func (ch *Command) executeShell(shellCmd string) {
415	shellCmd = strings.TrimSpace(shellCmd)
416	if shellCmd == "" {
417		ch.e.message = "No shell command specified"
418		return
419	}
420
421	// Execute the command using sh -c for proper shell interpretation.
422	cmd := exec.Command("/bin/sh", "-c", shellCmd)
423	output, err := cmd.CombinedOutput()
424
425	if err != nil {
426		// Display error along with any output that was produced.
427		if len(output) > 0 {
428			ch.e.message = fmt.Sprintf("Error: %v | Output: %s", err, strings.TrimSpace(string(output)))
429		} else {
430			ch.e.message = fmt.Sprintf("Error executing command: %v", err)
431		}
432		return
433	}
434
435	// Display the command output, truncating if too long.
436	outputStr := strings.TrimSpace(string(output))
437	if outputStr == "" {
438		ch.e.message = "Command executed successfully (no output)"
439	} else {
440		// Truncate output if it's too long for the message bar.
441		const maxLen = 200
442		if len(outputStr) > maxLen {
443			ch.e.message = outputStr[:maxLen] + "..."
444		} else {
445			ch.e.message = outputStr
446		}
447	}
448}
449
450// readShell runs a shell command and inserts the output into the buffer at cursor position.
451func (ch *Command) readShell(shellCmd string) {
452	shellCmd = strings.TrimSpace(shellCmd)
453	if shellCmd == "" {
454		ch.e.message = "No shell command specified"
455		return
456	}
457
458	b := ch.e.activeBuffer()
459	if b == nil {
460		return
461	}
462
463	if b.readOnly {
464		ch.e.message = "File is read-only"
465		return
466	}
467
468	// Execute the command using sh -c for proper shell interpretation.
469	cmd := exec.Command("/bin/sh", "-c", shellCmd)
470	output, err := cmd.CombinedOutput()
471
472	if err != nil {
473		// Display error along with any output that was produced.
474		if len(output) > 0 {
475			ch.e.message = fmt.Sprintf("Error: %v | Output: %s", err, strings.TrimSpace(string(output)))
476		} else {
477			ch.e.message = fmt.Sprintf("Error executing command: %v", err)
478		}
479		return
480	}
481
482	outputStr := string(output)
483	if outputStr == "" {
484		ch.e.message = "Command executed (no output to insert)"
485		return
486	}
487
488	// Save state for undo.
489	ch.e.saveState()
490
491	// Split output into lines and insert them into the buffer.
492	lines := strings.Split(outputStr, "\n")
493	// Remove trailing empty line if present (common with command output).
494	if len(lines) > 0 && lines[len(lines)-1] == "" {
495		lines = lines[:len(lines)-1]
496	}
497
498	if len(lines) == 0 {
499		ch.e.message = "Command executed (no output to insert)"
500		return
501	}
502
503	c := b.PrimaryCursor()
504	currentY := c.Y
505
506	// Insert output starting from the line after the cursor.
507	for i, line := range lines {
508		insertY := currentY + i + 1
509		// Create new line in buffer.
510		newLine := []rune(line)
511		// Insert the line into the buffer.
512		if insertY <= len(b.buffer) {
513			b.buffer = append(b.buffer[:insertY], append([][]rune{newLine}, b.buffer[insertY:]...)...)
514		} else {
515			b.buffer = append(b.buffer, newLine)
516		}
517	}
518
519	// Mark buffer as modified.
520	ch.e.markModified()
521
522	// Reparse syntax if needed.
523	if b.syntax != nil {
524		b.syntax.Reparse([]byte(b.toString()))
525	}
526
527	// Notify LSP of the change.
528	if b.lspClient != nil {
529		b.lspClient.SendDidChange(b.toString())
530	}
531
532	lineCount := len(lines)
533	if lineCount == 1 {
534		ch.e.message = "1 line inserted"
535	} else {
536		ch.e.message = fmt.Sprintf("%d lines inserted", lineCount)
537	}
538}