Allow multiple editor sessions (#221)

* Allow to get editor input from multiple sessions at a given time

* Add license
This commit is contained in:
Sung Won Cho 2019-07-13 13:02:09 +10:00 committed by GitHub
commit ddabdd9732
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 219 additions and 27 deletions

2
pkg/cli/.gitignore vendored
View file

@ -1,6 +1,6 @@
main
*.swo
tmp/
tmp*/
/vendor
test-dnote
/dist

View file

@ -115,8 +115,12 @@ func newRun(ctx context.DnoteCtx) infra.RunEFunc {
}
if content == "" {
fpath := ui.GetTmpContentPath(ctx)
err := ui.GetEditorInput(ctx, fpath, &content)
fpath, err := ui.GetTmpContentPath(ctx)
if err != nil {
return errors.Wrap(err, "getting temporarily content file path")
}
err = ui.GetEditorInput(ctx, fpath, &content)
if err != nil {
return errors.Wrap(err, "Failed to get editor input")
}

View file

@ -89,7 +89,10 @@ func newRun(ctx context.DnoteCtx) infra.RunEFunc {
}
if newContent == "" {
fpath := ui.GetTmpContentPath(ctx)
fpath, err := ui.GetTmpContentPath(ctx)
if err != nil {
return errors.Wrap(err, "getting temporarily content file path")
}
e := ioutil.WriteFile(fpath, []byte(oldContent), 0644)
if e != nil {

View file

@ -24,8 +24,10 @@ var (
DnoteDirName = ".dnote"
// DnoteDBFileName is a filename for the Dnote SQLite database
DnoteDBFileName = "dnote.db"
// TmpContentFilename is the filename for a temporary content
TmpContentFilename = "DNOTE_TMPCONTENT.md"
// TmpContentFileBase is the base for the filename for a temporary content
TmpContentFileBase = "DNOTE_TMPCONTENT"
// TmpContentFileExt is the extension for the temporary content file
TmpContentFileExt = "md"
// ConfigFilename is the name of the config file
ConfigFilename = "dnoterc"

View file

@ -315,7 +315,11 @@ func getEditorCommand() string {
func initDnoteDir(ctx context.DnoteCtx) error {
path := ctx.DnoteDir
if utils.FileExists(path) {
ok, err := utils.FileExists(path)
if err != nil {
return errors.Wrap(err, "checking if dnote dir exists")
}
if ok {
return nil
}
@ -329,7 +333,11 @@ func initDnoteDir(ctx context.DnoteCtx) error {
// initConfigFile populates a new config file if it does not exist yet
func initConfigFile(ctx context.DnoteCtx, apiEndpoint string) error {
path := config.GetPath(ctx)
if utils.FileExists(path) {
ok, err := utils.FileExists(path)
if err != nil {
return errors.Wrap(err, "checking if config exists")
}
if ok {
return nil
}

View file

@ -57,10 +57,19 @@ func TestInit(t *testing.T) {
db := database.OpenTestDB(t, opts.DnoteDir)
// Test
if !utils.FileExists(opts.DnoteDir) {
ok, err := utils.FileExists(opts.DnoteDir)
if err != nil {
t.Fatal(errors.Wrap(err, "checking if dnote dir exists"))
}
if !ok {
t.Errorf("dnote directory was not initialized")
}
if !utils.FileExists(fmt.Sprintf("%s/%s", opts.DnoteDir, consts.ConfigFilename)) {
ok, err = utils.FileExists(fmt.Sprintf("%s/%s", opts.DnoteDir, consts.ConfigFilename))
if err != nil {
t.Fatal(errors.Wrap(err, "checking if dnote config exists"))
}
if !ok {
t.Errorf("config file was not initialized")
}

View file

@ -85,7 +85,11 @@ func makeSchema(complete bool) schema {
func Legacy(ctx context.DnoteCtx) error {
// If schema does not exist, no need run a legacy migration
schemaPath := getSchemaPath(ctx)
if ok := utils.FileExists(schemaPath); !ok {
ok, err := utils.FileExists(schemaPath)
if err != nil {
return errors.Wrap(err, "checking if schema exists")
}
if !ok {
return nil
}
@ -472,7 +476,11 @@ var migrateToV8SystemKeyBookMark = "bookmark"
// migrateToV1 deletes YAML archive if exists
func migrateToV1(ctx context.DnoteCtx) error {
yamlPath := fmt.Sprintf("%s/%s", ctx.HomeDir, ".dnote-yaml-archived")
if !utils.FileExists(yamlPath) {
ok, err := utils.FileExists(yamlPath)
if err != nil {
return errors.Wrap(err, "checking if yaml file exists")
}
if !ok {
return nil
}

View file

@ -72,7 +72,11 @@ func TestMigrateToV1(t *testing.T) {
}
// test
if utils.FileExists(yamlPath) {
ok, err := utils.FileExists(yamlPath)
if err != nil {
t.Fatal(errors.Wrap(err, "checking if yaml file exists"))
}
if ok {
t.Fatal("YAML archive file has not been deleted")
}
})
@ -93,7 +97,11 @@ func TestMigrateToV1(t *testing.T) {
}
// test
if utils.FileExists(yamlPath) {
ok, err := utils.FileExists(yamlPath)
if err != nil {
t.Fatal(errors.Wrap(err, "checking if yaml file exists"))
}
if ok {
t.Fatal("YAML archive file must not exist")
}
})
@ -372,23 +380,43 @@ func TestMigrateToV8(t *testing.T) {
dnotercPath := fmt.Sprintf("%s/dnoterc", ctx.DnoteDir)
schemaFilePath := fmt.Sprintf("%s/schema", ctx.DnoteDir)
timestampFilePath := fmt.Sprintf("%s/timestamps", ctx.DnoteDir)
if ok := utils.FileExists(dnoteFilePath); ok {
ok, err := utils.FileExists(dnoteFilePath)
if err != nil {
t.Fatal(errors.Wrap(err, "checking if file exists"))
}
if ok {
t.Errorf("%s still exists", dnoteFilePath)
}
if ok := utils.FileExists(schemaFilePath); ok {
t.Errorf("%s still exists", dnoteFilePath)
ok, err = utils.FileExists(schemaFilePath)
if err != nil {
t.Fatal(errors.Wrap(err, "checking if file exists"))
}
if ok := utils.FileExists(timestampFilePath); ok {
t.Errorf("%s still exists", dnoteFilePath)
if ok {
t.Errorf("%s still exists", schemaFilePath)
}
if ok := utils.FileExists(dnotercPath); !ok {
t.Errorf("%s should exist", dnotercPath)
ok, err = utils.FileExists(timestampFilePath)
if err != nil {
t.Fatal(errors.Wrap(err, "checking if file exists"))
}
if ok {
t.Errorf("%s still exists", timestampFilePath)
}
ok, err = utils.FileExists(dnotercPath)
if err != nil {
t.Fatal(errors.Wrap(err, "checking if file exists"))
}
if !ok {
t.Errorf("%s still exists", dnotercPath)
}
// 2. test if notes and books are migrated
var bookCount, noteCount int
err := db.QueryRow("SELECT count(*) FROM books").Scan(&bookCount)
err = db.QueryRow("SELECT count(*) FROM books").Scan(&bookCount)
if err != nil {
panic(errors.Wrap(err, "counting books"))
}

View file

@ -34,8 +34,19 @@ import (
// GetTmpContentPath returns the path to the temporary file containing
// content being added or edited
func GetTmpContentPath(ctx context.DnoteCtx) string {
return fmt.Sprintf("%s/%s", ctx.DnoteDir, consts.TmpContentFilename)
func GetTmpContentPath(ctx context.DnoteCtx) (string, error) {
for i := 0; ; i++ {
filename := fmt.Sprintf("%s_%d.%s", consts.TmpContentFileBase, i, consts.TmpContentFileExt)
candidate := fmt.Sprintf("%s/%s", ctx.DnoteDir, filename)
ok, err := utils.FileExists(candidate)
if err != nil {
return "", errors.Wrapf(err, "checking if file exists at %s", candidate)
}
if !ok {
return candidate, nil
}
}
}
// getEditorCommand returns the system's editor command with appropriate flags,
@ -91,7 +102,11 @@ func newEditorCmd(ctx context.DnoteCtx, fpath string) (*exec.Cmd, error) {
// GetEditorInput gets the user input by launching a text editor and waiting for
// it to exit
func GetEditorInput(ctx context.DnoteCtx, fpath string, content *string) error {
if !utils.FileExists(fpath) {
ok, err := utils.FileExists(fpath)
if err != nil {
return errors.Wrapf(err, "checking if the file exists at %s", fpath)
}
if !ok {
f, err := os.Create(fpath)
if err != nil {
return errors.Wrap(err, "creating a temporary content file")

90
pkg/cli/ui/editor_test.go Normal file
View file

@ -0,0 +1,90 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote CLI.
*
* Dnote CLI is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote CLI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Dnote CLI. If not, see <https://www.gnu.org/licenses/>.
*/
package ui
import (
"fmt"
"os"
"testing"
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/cli/context"
"github.com/pkg/errors"
)
func TestGetTmpContentPath(t *testing.T) {
t.Run("no collision", func(t *testing.T) {
ctx := context.InitTestCtx(t, "../tmp1", nil)
defer context.TeardownTestCtx(t, ctx)
res, err := GetTmpContentPath(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
expected := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_0.md")
assert.Equal(t, res, expected, "filename did not match")
})
t.Run("one existing session", func(t *testing.T) {
// set up
ctx := context.InitTestCtx(t, "../tmp2", nil)
defer context.TeardownTestCtx(t, ctx)
p := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_0.md")
if _, err := os.Create(p); err != nil {
t.Fatal(errors.Wrap(err, "preparing the conflicting file"))
}
// execute
res, err := GetTmpContentPath(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
// test
expected := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_1.md")
assert.Equal(t, res, expected, "filename did not match")
})
t.Run("two existing sessions", func(t *testing.T) {
// set up
ctx := context.InitTestCtx(t, "../tmp3", nil)
defer context.TeardownTestCtx(t, ctx)
p1 := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_0.md")
if _, err := os.Create(p1); err != nil {
t.Fatal(errors.Wrap(err, "preparing the conflicting file"))
}
p2 := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_1.md")
if _, err := os.Create(p2); err != nil {
t.Fatal(errors.Wrap(err, "preparing the conflicting file"))
}
// execute
res, err := GetTmpContentPath(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
// test
expected := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_2.md")
assert.Equal(t, res, expected, "filename did not match")
})
}

View file

@ -44,9 +44,16 @@ func ReadFileAbs(relpath string) []byte {
}
// FileExists checks if the file exists at the given path
func FileExists(filepath string) bool {
func FileExists(filepath string) (bool, error) {
_, err := os.Stat(filepath)
return !os.IsNotExist(err)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, errors.Wrap(err, "getting file info")
}
// CopyDir copies a directory from src to dest, recursively copying nested

View file

@ -1,3 +1,21 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (