;;; ---------------------------------------------------------------------------
;;; TextSplit.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; BREAK A LINE OF TEXT INTO TWO
;;;
;;; PURPOSE
;;;   Splits a text object in two and puts the second half where you point.
;;;   For a note that has run too long for its box, a title that needs to wrap,
;;;   or a line of imported text that came in as one string when it should have
;;;   been three.
;;;
;;;   Three ways to say where to cut:
;;;     After     after a word you name - the usual way
;;;     Position  after a character count
;;;     Half      at the nearest space to the middle
;;;
;;;   The second piece keeps the height, style, rotation, width factor, layer
;;;   and colour of the original, so the two halves match.
;;;
;;; WHAT WAS FIXED
;;;   The original could not run. It read the text out of the object like this:
;;;
;;;       (Assoc (Quote 1.0) EntData)
;;;       (Assoc (Quote 40.0) EntData)
;;;       (Assoc (Quote 50.0) EntData)
;;;
;;;   Those group codes are written as REALS - 1.0, 40.0, 50.0 - but DXF group
;;;   codes are integers, and ASSOC compares with EQUAL, under which 1 and 1.0
;;;   are not the same thing. Every one of those lookups returned nil, so the
;;;   text came back as nil and the first (strlen nil) stopped it.
;;;
;;;   Its search loop was also unbounded. Looking for a word that was not in the
;;;   string, it walked off the end, where SUBSTR returns an empty string
;;;   forever - which never matches - so it hung until ESC.
;;;
;;;   Every one of its twenty variables was global, and it left CMDECHO set to 1
;;;   rather than to whatever it had been.
;;;
;;;   TEXTSPLIT  - break a line of text in two
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; SUPPORT
;;; ---------------------------------------------------------------------------

;;; Where a word starts in a string, or nil. Bounded by the string's own length,
;;; which is what the original lacked.
(defun Split:Find ( hay needle / i n limit )
    (setq n     (strlen needle)
          limit (1+ (- (strlen hay) n))
          i     1)
    (while (and (<= i limit) (/= (strcase (substr hay i n)) (strcase needle)))
        (setq i (1+ i)))
    (if (<= i limit) i)
)

;;; The space nearest the middle, so a split in half falls between words.
(defun Split:Middle ( s / mid i best )
    (setq mid (/ (strlen s) 2) i 1 best nil)
    (while (<= i (strlen s))
        (if (= " " (substr s i 1))
            (if (or (null best) (< (abs (- i mid)) (abs (- best mid))))
                (setq best i)))
        (setq i (1+ i)))
    best
)

;;; Everything from the original worth carrying to the second piece.
(defun Split:Props ( data / out )
    (foreach code '(8 6 62 370 7 40 41 50 51 71 210)
        (if (assoc code data) (setq out (cons (assoc code data) out))))
    (reverse out)
)

;;; ---------------------------------------------------------------------------
;;; MAIN COMMAND
;;; ---------------------------------------------------------------------------

(defun c:TEXTSPLIT ( / *error* vars vals sel ent data txt kind v word at
                       before after pt n )

    (setq vars '("CMDECHO" "OSMODE") vals (mapcar 'getvar vars))

    (defun Split:Restore ( )
        (mapcar 'setvar vars vals)
        (while (= 8 (logand 8 (getvar 'undoctl))) (command "_.UNDO" "_End"))
        (vl-catch-all-apply '(lambda ( ) (*pop-error-mode*)) '())
        (princ)
    )

    (defun *error* ( msg )
        (Split:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** TEXTSPLIT error: " msg " **")))
        (princ)
    )

    (setvar "CMDECHO" 0)
    ;; AutoCAD 2015 and later refuse (command) inside an *error* handler unless
    ;; the routine says up front that it will use one.
    (vl-catch-all-apply '(lambda ( ) (*push-error-using-command*)) '())
    (command "_.UNDO" "_Begin")

    (setq n 0)

    ;; Keep going until Enter, so a long note can be broken into several lines
    ;; one cut at a time.
    (while
        (progn
            (setq ent nil)
            (while (and (null ent)
                        (setq sel (entsel "\nText to split <Enter to finish>: ")))
                (setq kind (cdr (assoc 0 (entget (car sel)))))
                (cond
                    ((= kind "TEXT")  (setq ent (car sel)))
                    ((= kind "MTEXT")
                     (princ "\n  That is MTEXT - it already wraps on its own."))
                    (t (princ (strcat "\n  That is a " kind ", not text.")))))
            ent
        )

        (setq data (entget ent)
              txt  (cdr (assoc 1 data))
              at   nil)

        (initget "After Position Half")
        (setq v (getkword "\n  Split [After/Position/Half] <After>: "))
        (if (null v) (setq v "After"))

        (cond
            ((= v "After")
             (setq word (getstring t "\n    After which word: "))
             (if (= "" word)
                 (princ "\n    Nothing to look for.")
                 (if (setq at (Split:Find txt word))
                     ;; Cut after the word, not before it.
                     (setq at (+ at (strlen word) -1))
                     (princ (strcat "\n    \"" word "\" is not in that text.")))))

            ((= v "Position")
             (initget 6)
             (setq at (getint (strcat "\n    After how many characters (1 to "
                                      (itoa (strlen txt)) "): ")))
             (if (and at (>= at (strlen txt)))
                 (progn (princ "\n    That is the whole string.") (setq at nil))))

            (t (if (null (setq at (Split:Middle txt)))
                   (princ "\n    No space to split at - use Position."))))

        (if at
            (progn
                (setq before (substr txt 1 at)
                      after  (substr txt (1+ at)))

                ;; Trim the space at the join so neither piece starts or ends
                ;; with one.
                (while (and (> (strlen before) 0)
                            (= " " (substr before (strlen before) 1)))
                    (setq before (substr before 1 (1- (strlen before)))))
                (while (and (> (strlen after) 0) (= " " (substr after 1 1)))
                    (setq after (substr after 2)))

                (if (or (= "" before) (= "" after))
                    (princ "\n    That would leave one half empty.")
                    (progn
                        (setvar "OSMODE" 0)
                        (setq pt (getpoint "\n    Where should the second half go: "))
                        (if (null pt)
                            (princ "\n    Cancelled - nothing changed.")
                            (progn
                                ;; Shorten the original and make the remainder
                                ;; beside it.
                                (entmod (subst (cons 1 before) (assoc 1 data) data))
                                (entupd ent)
                                (entmake (append
                                    (list '(0 . "TEXT")
                                          (cons 10 pt) (cons 11 pt)
                                          (cons 1 after))
                                    (Split:Props data)))
                                (setq n (1+ n))
                                (princ (strcat "\n    Split into \"" before
                                               "\" and \"" after "\"."))))))))
    )

    (princ (strcat "\n" (itoa n) " split" (if (= n 1) "" "s") " made."))

    (Split:Restore)
    (princ)
)

(princ)
