1package main
2
3import (
4	"bytes"
5	"errors"
6	"io/ioutil"
7	"os"
8	"path/filepath"
9	"strconv"
10	"strings"
11
12	"github.com/xyproto/env"
13)
14
15var portalFilename = env.ExpandUser(filepath.Join(env.Str("TMPDIR", "/tmp"), env.Str("LOGNAME", "o")+"_portal.txt"))
16
17// Portal is a filename and a line number, for pulling text from
18type Portal struct {
19	absFilename string
20	lineNumber  LineNumber
21}
22
23// NewPortal returns a new portal to this filename and line number,
24// but does not save the new portal. Use the Save() method for that.
25func (e *Editor) NewPortal() (*Portal, error) {
26	absFilename, err := e.AbsFilename()
27	if err != nil {
28		return nil, err
29	}
30	return &Portal{absFilename, e.LineNumber()}, nil
31}
32
33// SameFile checks if the portal exist in the same file as the editor is editing
34func (p *Portal) SameFile(e *Editor) bool {
35	absFilename, err := e.AbsFilename()
36	if err != nil {
37		return false
38	}
39	return absFilename == p.absFilename
40}
41
42// MoveDown is useful when using portals within the same file.
43func (p *Portal) MoveDown() {
44	// PopLine handles overflows.
45	p.lineNumber++
46}
47
48// MoveUp is useful when using portals within the same file.
49func (p *Portal) MoveUp() {
50	// PopLine handles overflows.
51	p.lineNumber--
52}
53
54// ClosePortal will clear the portal by removing the portal file
55func ClosePortal(e *Editor) error {
56	e.sameFilePortal = nil
57	return os.Remove(portalFilename)
58}
59
60// HasPortal checks if a portal is currently active
61func HasPortal() bool {
62	return exists(portalFilename)
63}
64
65// LoadPortal will load a filename + line number from the portal.txt file
66func LoadPortal() (*Portal, error) {
67	data, err := ioutil.ReadFile(portalFilename)
68	if err != nil {
69		return nil, err
70	}
71	if !bytes.Contains(data, []byte{'\n'}) {
72		return nil, errors.New(portalFilename + " does not have a newline, it's not a portal file")
73	}
74	lines := strings.Split(string(data), "\n")
75	if len(lines) < 2 {
76		return nil, errors.New(portalFilename + " contains too few lines")
77	}
78	absFilename, err := filepath.Abs(lines[0])
79	if err != nil {
80		return nil, err
81	}
82	lineInt, err := strconv.Atoi(lines[1])
83	if err != nil {
84		return nil, err
85	}
86	lineNumber := LineNumber(lineInt)
87	return &Portal{absFilename, lineNumber}, nil
88}
89
90// LineIndex returns the current line index that the portal points to
91func (p *Portal) LineIndex() LineIndex {
92	return p.lineNumber.LineIndex()
93}
94
95// LineNumber returns the current line number that the portal points to
96func (p *Portal) LineNumber() LineNumber {
97	return p.lineNumber
98}
99
100// Save will save the portal
101func (p *Portal) Save() error {
102	s := p.absFilename + "\n" + p.lineNumber.String() + "\n"
103	// Anyone can read this file
104	if err := ioutil.WriteFile(portalFilename, []byte(s), 0600); err != nil {
105		return err
106	}
107	return os.Chmod(portalFilename, 0666)
108}
109
110// String returns the current portal (filename + line number) as a colon separated string
111func (p *Portal) String() string {
112	return filepath.Base(p.absFilename) + ":" + p.lineNumber.String()
113}
114
115// NewLineInserted reacts when the editor inserts a new line in the same file,
116// and moves the portal source one line down, if needed.
117func (p *Portal) NewLineInserted(y LineIndex) {
118	if y < p.LineIndex() {
119		p.MoveDown()
120	}
121}
122
123// PopLine removes (!) a line from the portal file, then removes that line
124func (p *Portal) PopLine(e *Editor, removeLine bool) (string, error) {
125	// popping a line from the same file is a special case
126	if p == e.sameFilePortal {
127		if removeLine {
128			return "", errors.New("not implemented") // not implemented and currently not in use
129		}
130		// The line moving is done by the editor InsertAbove and InsertBelow functions
131		return e.Line(p.LineIndex()), nil
132	}
133	data, err := ioutil.ReadFile(p.absFilename)
134	if err != nil {
135		return "", err
136	}
137	lines := strings.Split(string(data), "\n")
138	foundLine := ""
139	found := false
140	if removeLine {
141		modifiedLines := make([]string, 0, len(lines)-1)
142		for i, line := range lines {
143			if LineIndex(i) == p.lineNumber.LineIndex() {
144				foundLine = line
145				found = true
146			} else {
147				modifiedLines = append(modifiedLines, line)
148			}
149		}
150		if !found {
151			return "", errors.New("Could not pop line " + p.String())
152		}
153		data = []byte(strings.Join(modifiedLines, "\n"))
154		if err = ioutil.WriteFile(p.absFilename, data, 0600); err != nil {
155			return "", err
156		}
157	} else {
158		for i, line := range lines {
159			if LineIndex(i) == p.lineNumber.LineIndex() {
160				foundLine = line
161				found = true
162				break
163			}
164		}
165		if !found {
166			return "", errors.New("Could not pop line " + p.String())
167		}
168		// Now move the line number +1
169		p.lineNumber++
170		// And save the new portal
171		if err := p.Save(); err != nil {
172			return foundLine, err
173		}
174	}
175	return foundLine, nil
176}
177