Specs of Entities and Operations within a Programmer's Editor
Table of Contents
I use editors as target examples in Software Engineering courses I teach. The materials below are hence deliberately incomplete. The context of this article is Programmers' Editor
1 Meta Commentary
- Specs can be written in ordinary English. Almost always, these are highly ambiguous.
- Specs should be written in stylized English. They should be peer reviewed for the elimnation of ambiguity and to check for completeness. Re-read Meyer's Seven Sins of Specifiers.
- A certain amount of notations (formalism) always improves communication – when the notation is well chosen.
- Even when good notations are in use, always include well written specs in stylized English.
- For sequences, we use the familiar array [] notation.
- Let q be a sequence. #q is the number of elements in it. q[#] is the last item. q[ 0] is the first item.
- These notes implicitly reference items explained in Editors
- Please report errors. I am afraid it is not error-free. Ignore spaces within the = = token.
2 Text Elements
2.1 Lines
- A line, el, is a sequence of characters ending in '\n'. el[#] == '\n'
- Note the ambiguity. Is this "hello\nthere\n" a line?
- Excluding the last character, the line should have no '\n'.
- What about strange characters, such as control chars, …
- for i in 0 .. #el - 1: el[i] != '\n';
2.2 Text File Content
- Consider a text file tf. Its content is almost always thought of as a sequence of bytes. This is often the way the OS gives the content of a text file. This content can also be viewed as a sequence of lines.
is-a
You probably have heard of this from AI courses.- tf.content is-a seq of lines
- tf.content is-a seq of bytes
- There is an unambiguous way of mapping back-and-forth between these two: tf.content.lines and tf.content.bytes
3 Buffers
3.1 Buffer Data Type
- An edit buffer B has text content, B.content
- It may (not must) also have other "things": e.g., cursors, views/ windows, its file name, …
3.2 Buffer Operation: create
- create(file-name fnm) returns buffer b
- pre: fnm exists
- OS responsibility
- f := sys-call-open(fnm) + sys-call-read() + sys-call-close(); f non-null
- This is depending on "you-know-what-I-mean" and usurping some amount of "how" it is done.
- post: b.content = = f.content, f.name == fnm
- Requirements Clarification: If f does not exist, create an empty file, then do as above.
3.3 Buffer Operation: save
- save() returns file f
- f := sys-call-write(this-buffer.file-name, this-buffer.content)
- Requirements Clarification: If f is the file from which we created this buffer, silently replace it with new file content. Retain all file permission, file create time stamp etc. File modification is now.
3.4 Buffer Operations: insert, delete, cut, paste, undo, redo, …
- A spec should define all the operations.
3.5 Cut- Paste- Buffers
4 Cursors
4.1 Cursors Overview
- We use several cursors.
- dot and the mark: two must-have cursors
- Cursors exist in the buffers and views/windows.
- Cursors do not exist independently of a buffer or a window.
- A buffer has these cursors, but not necessarily "alive" at all times.
- Constraints (also known as invariants)
- Let b be a buffer; it has b.dot and b.mark cursors, among others.
- 0 <= b.mark.y < number of lines in b
- 0 <= b.mark.x < num chars of b.lines[b.mark.y].
- All of these constraints go into the class invariant of Buffer, whose data members include dot and mark
- Let b be a buffer; it has b.dot and b.mark cursors, among others.
4.2 Cursor Data Type
- We model a cursor as a pair x,y of numbers as in Cartesian coordinates. In our context, both are always non-negative integers, and the y grows downwards.
- Init values: dot = = mark = = (0, 0)
4.3 Cursor Operations
- Here are some suggestively named operations: move-right-by-one-char, move-right-by-one-word, move-right-by-one-line, move-right-by-one-sentence.
- Similar operations for "left" moves.
- In a spec suggestively naming is necessary but not sufficient. Exercises for you!
- The specs depend on definitions of word, line and sentences.
- The moves are constrained by the buffer that the cursor is on.
- Requirements Clarification: What happens if the cursor is at the end of the buffer, and we (try to) move right?
- Requirements Clarification: What happens if the cursor is at the right-bottom of the wind, and we (try to) move right?
5 Windows
5.1 Windows Overview
- The window content is "visible". We are not using the word in the sense of the Windows OS, or Linux KDE. We mean it as a rectangular area showing the content of some buffer. We can simplify the first sentence to: The window is visible.
- Requirements clarification: Unless a window is on a buffer B, the content of B is invisible.
- Requirements Clarification: Should we be able to edit B even when B is invisible? Our answer: No. Even though, it is very powerful to say yes.
- Requirements Clarification: Do windows overlap? Tiled? Left to the "GUI Window Manager of the System"?
- Requirements Clarification: It is perfect to map our windows into Windows OS or Linux KDE windows.
5.2 Windows Data Type
- A window W is of width ww, height hh. Window operations may
shrink, enlarge, move it relative to the buffer, etc.
- ww >= 0, hh >= 0
- Forbid == 0? Why? Why not? Requirements clarification?
- A window "has" a buffer B. The window content is dictated by this.
- A window has dot mark cursors.
5.3 Window Content
- The W.content is a portion of the buffer behind it.
- We specifiy the content t for the core (i.e., typical) case first. Then, we worry about fringe cases.
- W.content is a sequence of bytes. Does not have '\n'.
- #W.content = hh * ww
- Recall that W slides along its buffer B, only showing a portion of the buffer. From now on, we will visualize the B as a scroll.
- Relative to the buffer B, assume that the left-top corner of window
W is positioned at (wx, wy).
- W.content.lines[ 0] == Buffer.content.lines[wy], assuming h > 0
- W.content.lines[i] == Buffer.content.lines[wy + i -1] truncated to ww chars, for i: 0 .. hh - 1, in general
- What if Buffer.content.lines[wy + j], for some j, is too short? We pad it with blanks. Let us call W.content.lines[j] by the name el. Then, el = Buffer.content.lines[wy + j] + enough spaces so that #el = ww.
5.4 Window Operation: Place a Window on a Buffer
- Case 1: Buffer B has no windows on it.
- Case 2: Buffer B has exactly one window on it.
- Case 3: Buffer B has > 1 windows on it.
- We focus on Case 1 only. Next subsection.
5.5 Window Operation: Place a New Window on a Buffer
- createWin(buffer B, width w, height h) returns Window
- create a Window object w x h, so that its buffer is B, its dot and mark cursors are at (0, 0), left-top at (0, 0) of B, the content computed from B.
- Generally, we prefer "declarative" specs. Some times, it is easier to do this "operationally", as above.
5.6 Window Operation: Delete a Window
- Obviously the window is destroyed/ released.
- Requirements Clarfications: What happens to the buffer? Does this depend on whether the buf has other windows? Does the window have its own dot + mark? In addition to those of the buffer? Suppose the buffer has only this window, and now it is deleted. If then place a new window on it, should the new window have the last dot + marks?
5.7 Cursor in a Window
- Perennial Goal: The dot cursor should remain in the window.
- The mark cursor, if possible, should remain in the window. But may not be possible because the window is too small to contain both.
- Requirements Calrification: Both of these should have been described.
- Spec Decision: Are the (x, y) of the cursors relative? Relative to window? Or buffer?
- Requirements Calrification: Assume that the dot is at the right-bottom of the window. User wants to move right. Complain by beeping? Or move/ reposition the window appropritiately, honoring the wish?
6 Key Bindings
6.1 Key Bindings Overview
- Let QM be a sequence of public methods. So, e.g., QM[ 7] might be the creation of new buffer based on a file name you are yet to give.
- This QM is well defined at compile/ build time. It will not grow or shrink dynamically.
- Key bindings specifies what key stroke invokes what method of QM.
6.2 Key Bindings Data Type
- Spec predicated on a design idea: Have an array called KB, of size equal to the number of key "codes". For now, let us say we have 256 codes (based on ASCII 8-bit chars).
- KB[x] = = QM[i], 0 <= x < 256, for some i.
- The value of KB[x] can be changed by the user, at run time, or through a config file.
- Requiremenst Clarification: All the above.
- Design Suggestion: QM can be an array of pointers to public methods.
6.3 QM Self-Insert Method
- There is a Self-Insert Method. It has one parameter – the key code. Effect: inserts at the dot position this character.
- Most key bindings are to Self-Insert.
6.4 KB Operation: Display Current Bindings
6.5 KB Operation: Edit Bindings
6.6 KB Operation: Read Bindings from Config File
7 The Editor
7.1 The Editor Overview
- Requirements Clarification: Our editor should be able to edit multiple files simultaneously. It should permit cut-n-paste across these.
- Requirements Clarification: Our editor truly edits buffers, never files. Buffers are created from files. When a buffer is saved, it replaces the file it was created from.
- Requirements Clarification: New files are created by trying to edit a non-existing file.
7.2 Editor Operation: Start
- The editor as a process has already begun.
- Requirements Clarification: Start operation should resume where the user left off in the last session.
7.3 Editor Operation: Finish
- Exits (i.e., terminates) the editor process.
- Requirements Clarification: Warn and take helpful actions if hours of work by the user is about to be abandoned. Introduce the notion of last session.
7.4 Editor Operation: Loop
- It is essentially a non-terminating loop listening to key strokes and executing method bound to the keys.
- To exit the loop, user must invoke Finish.