aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model/forms.go
diff options
context:
space:
mode:
Diffstat (limited to 'gui/internal/model/forms.go')
-rw-r--r--gui/internal/model/forms.go57
1 files changed, 56 insertions, 1 deletions
diff --git a/gui/internal/model/forms.go b/gui/internal/model/forms.go
index f0a3983..290bbc8 100644
--- a/gui/internal/model/forms.go
+++ b/gui/internal/model/forms.go
@@ -88,16 +88,46 @@ func (r *Rules) ReplaceForm(i int, text string) error {
// DeleteForm removes form i and the comment lines directly above it - its
// block (GUI design ยง5.1). A comment separated from the form by a blank
// line belongs to the file, not to the form, and stays.
+//
+// A block is whole lines, which is wrong when another form shares one:
+// deleting the first of "(exclude A) (exclude B)" would take B as well,
+// silently, leaving a file that still parses and still checks clean. When
+// the block would reach another form, only the form's own text goes, with
+// the spaces that followed it.
func (r *Rules) DeleteForm(i int) error {
- f, err := r.form(i)
+ forms, err := r.Forms()
if err != nil {
return err
}
+ if i < 0 || i >= len(forms) {
+ return fmt.Errorf("model: no form %d", i)
+ }
+ f := forms[i]
start, end := r.block(f)
+ if r.blockTouchesAnotherForm(forms, i, start, end) {
+ start, end = f.Pos.Offset, f.End.Offset
+ for end < len(r.Text) && (r.Text[end] == ' ' || r.Text[end] == '\t') {
+ end++
+ }
+ }
r.Text = join(r.Text[:start], r.Text[end:])
return nil
}
+// blockTouchesAnotherForm reports whether the span start..end covers any
+// part of a form other than forms[i].
+func (r *Rules) blockTouchesAnotherForm(forms []Form, i, start, end int) bool {
+ for j, other := range forms {
+ if j == i {
+ continue
+ }
+ if other.Pos.Offset < end && other.End.Offset > start {
+ return true
+ }
+ }
+ return false
+}
+
// MoveForm moves form i one place up (delta -1) or down (delta 1), with its
// comments. Moving past either end does nothing.
func (r *Rules) MoveForm(i, delta int) error {
@@ -222,6 +252,31 @@ func (r *Rules) block(f Form) (start, end int) {
return start, end
}
+// clearForm removes the form spanning start..end from text. It takes the
+// whole line - with the comment trailing it, and the blank it would leave
+// behind - only when the line holds nothing else. A setting written beside
+// something else, which is what the Settings window itself writes when it
+// creates "(defaults\n (case ignore))", loses only itself: taking the line
+// took the closing paren of defaults with it and broke the file.
+func clearForm(text string, start, end int) string {
+ ls, le := lineStart(text, start), lineEnd(text, end)
+ before := strings.TrimSpace(text[ls:start])
+ after := text[end:le]
+ if i := strings.IndexByte(after, ';'); i >= 0 {
+ // A comment after it on the same line describes it, and goes too.
+ after = after[:i]
+ }
+ if before == "" && strings.TrimSpace(after) == "" {
+ return join(text[:ls], text[le:])
+ }
+ // Something else shares the line: drop the form and the spaces that
+ // followed it, and nothing more.
+ for end < len(text) && (text[end] == ' ' || text[end] == '\t') {
+ end++
+ }
+ return text[:start] + text[end:]
+}
+
// lineStart is the offset just after the newline before at.
func lineStart(s string, at int) int {
if at > len(s) {