Ver Fonte

Add auto-indentation to the tvision-m2 editor

- Enter copies the current line's leading spaces and adds one indent step
  after an open keyword (BEGIN, THEN, ELSE, ELSIF, DO, LOOP, REPEAT, RECORD,
  CASE, OF).
- A line whose first word is a close keyword (END, UNTIL, ELSE, ELSIF) is
  dedented one step when the word is completed.
- New EditorSetIndent(data, n) sets the step in spaces (default 2, 0 turns
  the extra step off while still copying leading whitespace).
- Keyword utilities were moved ahead of the editing routines so the
  indent/dedent helpers can use them.
- examples/edit: File -> New now clears to an empty buffer and re-applies
  the language/indent; docs updated.
Eric Streit há 3 semanas atrás
pai
commit
b4b1ecbb96

+ 7 - 0
tvision-m2/API.md

@@ -127,6 +127,7 @@ tv.FocusView(ev);
 | `EditorSave(data, path) : BOOLEAN` | save the buffer to a text file |
 | `EditorDirty(data) : BOOLEAN` | TRUE if modified since load/save |
 | `EditorSetLanguage(data, lang)` | enable highlighting for a language |
+| `EditorSetIndent(data, n)` | auto-indent step in spaces (default 2; 0 = off) |
 
 Language constants: `langNone`, `langModula2`, `langPascal`, `langOberon`.
 With a language set the editor highlights keywords (white), strings (red),
@@ -135,6 +136,12 @@ Pascal/Modula-2) and numbers (red).  For Modula-2 and Oberon, finishing a
 word with a delimiter (space, Tab, Enter or punctuation) automatically
 upper-cases it if it is a keyword (`begin` -> `BEGIN`).
 
+Auto-indentation (when a language is set): Enter copies the current line's
+leading spaces and adds one indent step after an open keyword (`BEGIN`,
+`THEN`, `ELSE`, `ELSIF`, `DO`, `LOOP`, `REPEAT`, `RECORD`, `CASE`, `OF`);
+a line whose first word is a close keyword (`END`, `UNTIL`, `ELSE`, `ELSIF`)
+is dedented one step.  Configure the step with `EditorSetIndent`.
+
 Keys: printable text, Enter, Backspace, Delete, arrows, Home/End, PgUp/PgDn,
 Tab (indent to a 4-column stop), Ins (insert/overwrite).  The mouse places
 the cursor and the wheel scrolls.

+ 2 - 1
tvision-m2/README.md

@@ -54,7 +54,8 @@ sibling project:
   file (`EditorLoad`/`EditorSave`).
 - Syntax highlighting for Modula-2, Pascal and Oberon (keywords, strings,
   numbers, `(* *)`/`{ }`/`//` comments) and automatic keyword up-casing for
-  Modula-2 and Oberon (`begin` -> `BEGIN`).
+  Modula-2 and Oberon (`begin` -> `BEGIN`), plus auto-indentation (indent
+  after `BEGIN`/`THEN`/`DO`/..., dedent on `END`/`UNTIL`/`ELSE`).
 - Widgets: static text, single/double frame, input line (insert, backspace,
   delete, Home/End/arrows), push button, check box, radio buttons, list box
   with selection and wheel scrolling, vertical scroll bar.

+ 3 - 1
tvision-m2/SUMMARY.md

@@ -47,7 +47,9 @@ from the captured terminal stream).
 - Multi-line editor widget (`VEditor`) with insert/overwrite, navigation,
   Tab indent, mouse positioning, a Ln/Col/Insert/Modified status row and
   file load/save; syntax highlighting for Modula-2/Pascal/Oberon and
-  automatic keyword up-casing for Modula-2/Oberon; demo `examples/edit`
+  automatic keyword up-casing and auto-indentation for Modula-2/Oberon
+  (indent after open keywords, dedent on close keywords); demo
+  `examples/edit`
   (with a Syntax menu to switch language).
 - Example programs (`empty`, `window`, `dialog`, `menus`, `edit`, `gfx`); API
   reference (`API.md`) and tutorial (`TUTORIAL.md`).

+ 1 - 0
tvision-m2/TUTORIAL.md

@@ -152,6 +152,7 @@ IF NOT tv.EditorLoad(edp, "notes.txt") THEN
     tv.EditorInit(edp); tv.EditorAddLine(edp, "Type here...")
 END;
 tv.EditorSetLanguage(edp, tv.langModula2);   (* or langPascal / langOberon *)
+tv.EditorSetIndent(edp, 2);                  (* auto-indent: 2 spaces per level; 0 = off *)
 ev := tv.NewView(tv.VEditor, 2, 2, 60, 20);
 tv.EditorAttach(ev, edp);
 tv.AddView(win, ev);

BIN
tvision-m2/demo


BIN
tvision-m2/examples/dialog


BIN
tvision-m2/examples/edit


+ 5 - 1
tvision-m2/examples/edit.mod

@@ -80,6 +80,7 @@ BEGIN
 
     IF NOT tv.EditorLoad(edp, "tvision-edit.txt") THEN Sample END;
     tv.EditorSetLanguage(edp, tv.langModula2);
+    tv.EditorSetIndent(edp, 2);
 
     edView := tv.NewView(tv.VEditor, 2, 2, tv.Cols() - 4, tv.Rows() - 5);
     tv.EditorAttach(edView, CAST(tv.EditorDataPtr, ADR(ed)));
@@ -99,7 +100,10 @@ BEGIN
                 IF cmd = mQuit THEN
                     quit := TRUE
                 ELSIF cmd = mNew THEN
-                    Sample; Refresh
+                    tv.EditorInit(edp);
+                    tv.EditorSetLanguage(edp, tv.langModula2);
+                    tv.EditorSetIndent(edp, 2);
+                    Refresh
                 ELSIF cmd = mSave THEN
                     IF tv.EditorSave(edp, "tvision-edit.txt") THEN
                         tv.Message("Save", "Saved tvision-edit.txt.")

BIN
tvision-m2/examples/empty


BIN
tvision-m2/examples/gfx


BIN
tvision-m2/examples/menus


BIN
tvision-m2/examples/window


+ 2 - 0
tvision-m2/tv.def

@@ -139,6 +139,7 @@ TYPE
                      ins   : BOOLEAN;      (* insert mode *)
                      dirty : BOOLEAN;
                      lang  : INTEGER;      (* one of the lang* constants *)
+                     indent : INTEGER;     (* auto-indent step (0 = off) *)
                  END;
     EditorDataPtr = POINTER TO EditorData;
 
@@ -217,5 +218,6 @@ PROCEDURE EditorLoad (data : EditorDataPtr; path : ARRAY OF CHAR) : BOOLEAN;
 PROCEDURE EditorSave (data : EditorDataPtr; path : ARRAY OF CHAR) : BOOLEAN;
 PROCEDURE EditorDirty (data : EditorDataPtr) : BOOLEAN;
 PROCEDURE EditorSetLanguage (data : EditorDataPtr; lang : INTEGER);
+PROCEDURE EditorSetIndent (data : EditorDataPtr; n : INTEGER);
 
 END tv.

+ 166 - 79
tvision-m2/tv.mod

@@ -1091,6 +1091,84 @@ BEGIN
     IF data^.cx > data^.len[data^.cy] THEN data^.cx := data^.len[data^.cy] END
 END EdEnsure;
 
+PROCEDURE Eq (a, b : ARRAY OF CHAR) : BOOLEAN;
+    VAR i : INTEGER;
+BEGIN
+    i := 0;
+    WHILE (i <= VAL(INTEGER, HIGH(a))) AND (i <= VAL(INTEGER, HIGH(b))) AND
+          (a[i] # 0C) AND (b[i] # 0C) AND (a[i] = b[i]) DO
+        i := i + 1
+    END;
+    RETURN a[i] = b[i]
+END Eq;
+
+PROCEDURE IsIdentChar (c : CHAR) : BOOLEAN;
+BEGIN
+    RETURN ((c >= 'A') AND (c <= 'Z')) OR ((c >= 'a') AND (c <= 'z')) OR
+           ((c >= '0') AND (c <= '9')) OR (c = '_')
+END IsIdentChar;
+
+PROCEDURE UpcaseCopy (VAR dest : ARRAY OF CHAR; s : ARRAY OF CHAR);
+    VAR i : INTEGER; c : CHAR;
+BEGIN
+    i := 0;
+    WHILE (i <= VAL(INTEGER, HIGH(s))) AND (i < VAL(INTEGER, HIGH(dest))) AND (s[i] # 0C) DO
+        c := s[i];
+        IF (c >= 'a') AND (c <= 'z') THEN c := CHR(ORD(c) - 32) END;
+        dest[i] := c;
+        i := i + 1
+    END;
+    dest[i] := 0C
+END UpcaseCopy;
+
+PROCEDURE IsKeywordM2 (w : ARRAY OF CHAR) : BOOLEAN;
+BEGIN
+    RETURN Eq(w,"AND") OR Eq(w,"ARRAY") OR Eq(w,"BEGIN") OR Eq(w,"BY") OR
+           Eq(w,"CASE") OR Eq(w,"CONST") OR Eq(w,"DEFINITION") OR Eq(w,"DIV") OR
+           Eq(w,"DO") OR Eq(w,"ELSE") OR Eq(w,"ELSIF") OR Eq(w,"END") OR
+           Eq(w,"EXIT") OR Eq(w,"EXPORT") OR Eq(w,"FOR") OR Eq(w,"FROM") OR
+           Eq(w,"IF") OR Eq(w,"IMPLEMENTATION") OR Eq(w,"IMPORT") OR Eq(w,"IN") OR
+           Eq(w,"LOOP") OR Eq(w,"MOD") OR Eq(w,"MODULE") OR Eq(w,"NOT") OR
+           Eq(w,"OF") OR Eq(w,"OR") OR Eq(w,"POINTER") OR Eq(w,"PROCEDURE") OR
+           Eq(w,"QUALIFIED") OR Eq(w,"RECORD") OR Eq(w,"REPEAT") OR Eq(w,"RETURN") OR
+           Eq(w,"SET") OR Eq(w,"THEN") OR Eq(w,"TO") OR Eq(w,"TYPE") OR
+           Eq(w,"UNTIL") OR Eq(w,"VAR") OR Eq(w,"WHILE") OR Eq(w,"WITH")
+END IsKeywordM2;
+
+PROCEDURE IsKeywordOberon (w : ARRAY OF CHAR) : BOOLEAN;
+BEGIN
+    RETURN Eq(w,"ARRAY") OR Eq(w,"BEGIN") OR Eq(w,"BY") OR Eq(w,"CASE") OR
+           Eq(w,"CONST") OR Eq(w,"DIV") OR Eq(w,"DO") OR Eq(w,"ELSE") OR
+           Eq(w,"ELSIF") OR Eq(w,"END") OR Eq(w,"FALSE") OR Eq(w,"FOR") OR
+           Eq(w,"IF") OR Eq(w,"IMPORT") OR Eq(w,"IN") OR Eq(w,"IS") OR
+           Eq(w,"MOD") OR Eq(w,"MODULE") OR Eq(w,"NIL") OR Eq(w,"OF") OR
+           Eq(w,"OR") OR Eq(w,"POINTER") OR Eq(w,"PROCEDURE") OR Eq(w,"RECORD") OR
+           Eq(w,"REPEAT") OR Eq(w,"RETURN") OR Eq(w,"THEN") OR Eq(w,"TO") OR
+           Eq(w,"TRUE") OR Eq(w,"TYPE") OR Eq(w,"UNTIL") OR Eq(w,"VAR") OR
+           Eq(w,"WHILE") OR Eq(w,"WITH")
+END IsKeywordOberon;
+
+PROCEDURE IsKeywordPascal (w : ARRAY OF CHAR) : BOOLEAN;
+BEGIN
+    RETURN Eq(w,"AND") OR Eq(w,"ARRAY") OR Eq(w,"BEGIN") OR Eq(w,"CASE") OR
+           Eq(w,"CONST") OR Eq(w,"DIV") OR Eq(w,"DO") OR Eq(w,"DOWNTO") OR
+           Eq(w,"ELSE") OR Eq(w,"END") OR Eq(w,"FILE") OR Eq(w,"FOR") OR
+           Eq(w,"FUNCTION") OR Eq(w,"GOTO") OR Eq(w,"IF") OR Eq(w,"IN") OR
+           Eq(w,"LABEL") OR Eq(w,"MOD") OR Eq(w,"NIL") OR Eq(w,"NOT") OR
+           Eq(w,"OF") OR Eq(w,"OR") OR Eq(w,"PACKED") OR Eq(w,"PROCEDURE") OR
+           Eq(w,"PROGRAM") OR Eq(w,"RECORD") OR Eq(w,"REPEAT") OR Eq(w,"SET") OR
+           Eq(w,"THEN") OR Eq(w,"TO") OR Eq(w,"TYPE") OR Eq(w,"UNTIL") OR
+           Eq(w,"VAR") OR Eq(w,"WHILE") OR Eq(w,"WITH")
+END IsKeywordPascal;
+
+PROCEDURE IsKeywordLang (lang : INTEGER; w : ARRAY OF CHAR) : BOOLEAN;
+BEGIN
+    IF lang = langModula2 THEN RETURN IsKeywordM2(w) END;
+    IF lang = langOberon THEN RETURN IsKeywordOberon(w) END;
+    IF lang = langPascal THEN RETURN IsKeywordPascal(w) END;
+    RETURN FALSE
+END IsKeywordLang;
+
 PROCEDURE EdInsertChar (data : EditorDataPtr; cp : CARDINAL);
     VAR ln, i : INTEGER;
 BEGIN
@@ -1113,8 +1191,69 @@ BEGIN
     data^.dirty := TRUE
 END EdInsertChar;
 
+PROCEDURE IsOpenKw (w : ARRAY OF CHAR) : BOOLEAN;
+BEGIN
+    RETURN Eq(w,"BEGIN") OR Eq(w,"THEN") OR Eq(w,"ELSE") OR Eq(w,"ELSIF") OR
+           Eq(w,"DO") OR Eq(w,"LOOP") OR Eq(w,"REPEAT") OR Eq(w,"RECORD") OR
+           Eq(w,"CASE") OR Eq(w,"OF")
+END IsOpenKw;
+
+PROCEDURE IsCloseKw (w : ARRAY OF CHAR) : BOOLEAN;
+BEGIN
+    RETURN Eq(w,"END") OR Eq(w,"UNTIL") OR Eq(w,"ELSE") OR Eq(w,"ELSIF")
+END IsCloseKw;
+
+PROCEDURE EdIndentSpaces (data : EditorDataPtr; ln : INTEGER) : INTEGER;
+    VAR i : INTEGER;
+BEGIN
+    i := 0;
+    WHILE (i < data^.len[ln]) AND (data^.line[ln][i] = ' ') DO i := i + 1 END;
+    RETURN i
+END EdIndentSpaces;
+
+PROCEDURE EdRtrim (data : EditorDataPtr; ln, upto : INTEGER) : INTEGER;
+BEGIN
+    WHILE (upto > 0) AND (data^.line[ln][upto - 1] = ' ') DO upto := upto - 1 END;
+    RETURN upto
+END EdRtrim;
+
+PROCEDURE EdLastWordOpen (data : EditorDataPtr; ln : INTEGER) : BOOLEAN;
+    VAR n, i : INTEGER; w : ARRAY [0..63] OF CHAR;
+BEGIN
+    IF data^.lang = langNone THEN RETURN FALSE END;
+    n := EdRtrim(data, ln, data^.len[ln]);
+    IF n = 0 THEN RETURN FALSE END;
+    i := n;
+    WHILE (i > 0) AND IsIdentChar(data^.line[ln][i - 1]) DO i := i - 1 END;
+    n := 0;
+    WHILE (i < data^.len[ln]) AND IsIdentChar(data^.line[ln][i]) AND (n < 63) DO
+        w[n] := data^.line[ln][i]; n := n + 1; i := i + 1
+    END;
+    w[n] := 0C;
+    UpcaseCopy(w, w);
+    RETURN IsOpenKw(w)
+END EdLastWordOpen;
+
+PROCEDURE EdDedent (data : EditorDataPtr);
+    VAR lead, n, i : INTEGER;
+BEGIN
+    IF (data = NIL) OR (data^.lang = langNone) OR (data^.indent <= 0) THEN RETURN END;
+    lead := EdIndentSpaces(data, data^.cy);
+    IF lead = 0 THEN RETURN END;
+    n := data^.indent;
+    IF n > lead THEN n := lead END;
+    FOR i := 0 TO data^.len[data^.cy] - n - 1 DO
+        data^.line[data^.cy][i] := data^.line[data^.cy][i + n]
+    END;
+    data^.len[data^.cy] := data^.len[data^.cy] - n;
+    data^.line[data^.cy][data^.len[data^.cy]] := 0C;
+    data^.cx := data^.cx - n;
+    IF data^.cx < 0 THEN data^.cx := 0 END;
+    data^.dirty := TRUE
+END EdDedent;
+
 PROCEDURE EdNewLine (data : EditorDataPtr);
-    VAR ln, i, k, tail : INTEGER;
+    VAR ln, i, k, tail, ind : INTEGER;
 BEGIN
     EdEnsure(data);
     IF data^.count >= EDMAXLINES THEN RETURN END;
@@ -1131,9 +1270,22 @@ BEGIN
     data^.len[ln + 1] := tail;
     FOR k := data^.cx TO EDMAXCOL - 1 DO data^.line[ln][k] := 0C END;
     data^.len[ln] := data^.cx;
+    (* auto-indent: copy the leading spaces, plus one level after an open
+       keyword (BEGIN/THEN/DO/... ) *)
+    ind := EdIndentSpaces(data, ln);
+    IF EdLastWordOpen(data, ln) AND (data^.indent > 0) THEN
+        ind := ind + data^.indent
+    END;
+    IF (ind > 0) AND (data^.len[ln + 1] + ind < EDMAXCOL - 1) THEN
+        FOR k := data^.len[ln + 1] - 1 TO 0 BY -1 DO
+            data^.line[ln + 1][k + ind] := data^.line[ln + 1][k]
+        END;
+        FOR k := 0 TO ind - 1 DO data^.line[ln + 1][k] := ' ' END;
+        data^.len[ln + 1] := data^.len[ln + 1] + ind
+    END;
     data^.count := data^.count + 1;
     data^.cy := data^.cy + 1;
-    data^.cx := 0;
+    data^.cx := ind;
     data^.dirty := TRUE
 END EdNewLine;
 
@@ -1241,83 +1393,7 @@ END EdMove;
 
 (* --- syntax highlighting + keyword upcasing ---------------------------- *)
 
-PROCEDURE Eq (a, b : ARRAY OF CHAR) : BOOLEAN;
-    VAR i : INTEGER;
-BEGIN
-    i := 0;
-    WHILE (i <= VAL(INTEGER, HIGH(a))) AND (i <= VAL(INTEGER, HIGH(b))) AND
-          (a[i] # 0C) AND (b[i] # 0C) AND (a[i] = b[i]) DO
-        i := i + 1
-    END;
-    RETURN a[i] = b[i]
-END Eq;
 
-PROCEDURE IsIdentChar (c : CHAR) : BOOLEAN;
-BEGIN
-    RETURN ((c >= 'A') AND (c <= 'Z')) OR ((c >= 'a') AND (c <= 'z')) OR
-           ((c >= '0') AND (c <= '9')) OR (c = '_')
-END IsIdentChar;
-
-PROCEDURE UpcaseCopy (VAR dest : ARRAY OF CHAR; s : ARRAY OF CHAR);
-    VAR i : INTEGER; c : CHAR;
-BEGIN
-    i := 0;
-    WHILE (i <= VAL(INTEGER, HIGH(s))) AND (i < VAL(INTEGER, HIGH(dest))) AND (s[i] # 0C) DO
-        c := s[i];
-        IF (c >= 'a') AND (c <= 'z') THEN c := CHR(ORD(c) - 32) END;
-        dest[i] := c;
-        i := i + 1
-    END;
-    dest[i] := 0C
-END UpcaseCopy;
-
-PROCEDURE IsKeywordM2 (w : ARRAY OF CHAR) : BOOLEAN;
-BEGIN
-    RETURN Eq(w,"AND") OR Eq(w,"ARRAY") OR Eq(w,"BEGIN") OR Eq(w,"BY") OR
-           Eq(w,"CASE") OR Eq(w,"CONST") OR Eq(w,"DEFINITION") OR Eq(w,"DIV") OR
-           Eq(w,"DO") OR Eq(w,"ELSE") OR Eq(w,"ELSIF") OR Eq(w,"END") OR
-           Eq(w,"EXIT") OR Eq(w,"EXPORT") OR Eq(w,"FOR") OR Eq(w,"FROM") OR
-           Eq(w,"IF") OR Eq(w,"IMPLEMENTATION") OR Eq(w,"IMPORT") OR Eq(w,"IN") OR
-           Eq(w,"LOOP") OR Eq(w,"MOD") OR Eq(w,"MODULE") OR Eq(w,"NOT") OR
-           Eq(w,"OF") OR Eq(w,"OR") OR Eq(w,"POINTER") OR Eq(w,"PROCEDURE") OR
-           Eq(w,"QUALIFIED") OR Eq(w,"RECORD") OR Eq(w,"REPEAT") OR Eq(w,"RETURN") OR
-           Eq(w,"SET") OR Eq(w,"THEN") OR Eq(w,"TO") OR Eq(w,"TYPE") OR
-           Eq(w,"UNTIL") OR Eq(w,"VAR") OR Eq(w,"WHILE") OR Eq(w,"WITH")
-END IsKeywordM2;
-
-PROCEDURE IsKeywordOberon (w : ARRAY OF CHAR) : BOOLEAN;
-BEGIN
-    RETURN Eq(w,"ARRAY") OR Eq(w,"BEGIN") OR Eq(w,"BY") OR Eq(w,"CASE") OR
-           Eq(w,"CONST") OR Eq(w,"DIV") OR Eq(w,"DO") OR Eq(w,"ELSE") OR
-           Eq(w,"ELSIF") OR Eq(w,"END") OR Eq(w,"FALSE") OR Eq(w,"FOR") OR
-           Eq(w,"IF") OR Eq(w,"IMPORT") OR Eq(w,"IN") OR Eq(w,"IS") OR
-           Eq(w,"MOD") OR Eq(w,"MODULE") OR Eq(w,"NIL") OR Eq(w,"OF") OR
-           Eq(w,"OR") OR Eq(w,"POINTER") OR Eq(w,"PROCEDURE") OR Eq(w,"RECORD") OR
-           Eq(w,"REPEAT") OR Eq(w,"RETURN") OR Eq(w,"THEN") OR Eq(w,"TO") OR
-           Eq(w,"TRUE") OR Eq(w,"TYPE") OR Eq(w,"UNTIL") OR Eq(w,"VAR") OR
-           Eq(w,"WHILE") OR Eq(w,"WITH")
-END IsKeywordOberon;
-
-PROCEDURE IsKeywordPascal (w : ARRAY OF CHAR) : BOOLEAN;
-BEGIN
-    RETURN Eq(w,"AND") OR Eq(w,"ARRAY") OR Eq(w,"BEGIN") OR Eq(w,"CASE") OR
-           Eq(w,"CONST") OR Eq(w,"DIV") OR Eq(w,"DO") OR Eq(w,"DOWNTO") OR
-           Eq(w,"ELSE") OR Eq(w,"END") OR Eq(w,"FILE") OR Eq(w,"FOR") OR
-           Eq(w,"FUNCTION") OR Eq(w,"GOTO") OR Eq(w,"IF") OR Eq(w,"IN") OR
-           Eq(w,"LABEL") OR Eq(w,"MOD") OR Eq(w,"NIL") OR Eq(w,"NOT") OR
-           Eq(w,"OF") OR Eq(w,"OR") OR Eq(w,"PACKED") OR Eq(w,"PROCEDURE") OR
-           Eq(w,"PROGRAM") OR Eq(w,"RECORD") OR Eq(w,"REPEAT") OR Eq(w,"SET") OR
-           Eq(w,"THEN") OR Eq(w,"TO") OR Eq(w,"TYPE") OR Eq(w,"UNTIL") OR
-           Eq(w,"VAR") OR Eq(w,"WHILE") OR Eq(w,"WITH")
-END IsKeywordPascal;
-
-PROCEDURE IsKeywordLang (lang : INTEGER; w : ARRAY OF CHAR) : BOOLEAN;
-BEGIN
-    IF lang = langModula2 THEN RETURN IsKeywordM2(w) END;
-    IF lang = langOberon THEN RETURN IsKeywordOberon(w) END;
-    IF lang = langPascal THEN RETURN IsKeywordPascal(w) END;
-    RETURN FALSE
-END IsKeywordLang;
 
 PROCEDURE HiPut (v : ViewPtr; c : INTEGER; ch : CHAR; attr : Attr;
                  x0, y0, w : INTEGER; draw : BOOLEAN);
@@ -1435,6 +1511,10 @@ BEGIN
     IF IsKeywordLang(data^.lang, up) AND (NOT Eq(w, up)) THEN
         FOR k := 0 TO n - 1 DO data^.line[ln][i + k] := up[k] END;
         data^.dirty := TRUE
+    END;
+    (* smart dedent: a close keyword at the start of the line moves it left *)
+    IF IsCloseKw(up) AND (i = EdIndentSpaces(data, ln)) THEN
+        EdDedent(data)
     END
 END EdUpcaseWord;
 
@@ -1505,7 +1585,8 @@ BEGIN
         EdClrLine(data^.line[0]);
         data^.len[0] := 0;
         data^.cx := 0; data^.cy := 0; data^.top := 0; data^.left := 0;
-        data^.ins := TRUE; data^.dirty := FALSE; data^.lang := langNone
+        data^.ins := TRUE; data^.dirty := FALSE; data^.lang := langNone;
+        data^.indent := 2
     END
 END EditorInit;
 
@@ -1559,6 +1640,7 @@ BEGIN
     END;
     data^.cx := 0; data^.cy := 0; data^.top := 0; data^.left := 0;
     data^.dirty := FALSE;
+    IF data^.indent <= 0 THEN data^.indent := 2 END;
     RETURN TRUE
 END EditorLoad;
 
@@ -1588,6 +1670,11 @@ BEGIN
     IF data # NIL THEN data^.lang := lang END
 END EditorSetLanguage;
 
+PROCEDURE EditorSetIndent (data : EditorDataPtr; n : INTEGER);
+BEGIN
+    IF data # NIL THEN data^.indent := n END
+END EditorSetIndent;
+
 PROCEDURE DrawView(v : ViewPtr);
     VAR c : ViewPtr;
 BEGIN

BIN
tvision-m2/tv.o