github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/portal.go (about)

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