1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-10 18:10:49 +09:00

Merge remote-tracking branch 'origin/main' into feature/spaces

# Conflicts:
#	core/block/editor/clipboard/clipboard_test.go
#	go.mod
#	go.sum
This commit is contained in:
Mikhail Iudin 2023-09-15 16:30:55 +02:00
commit 4d7c2f7f54
No known key found for this signature in database
GPG key ID: FAAAA8BAABDFF1C0
10 changed files with 230 additions and 32 deletions

View file

@ -193,7 +193,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Install Go
uses: actions/setup-go@v1
uses: actions/setup-go@v4
with:
go-version: '~1.20.8'
- name: Setup GO

View file

@ -12,7 +12,7 @@ jobs:
GOPRIVATE: github.com/anyproto
steps:
- name: Install Go
uses: actions/setup-go@v1
uses: actions/setup-go@v4
with:
go-version: "~1.20.8"

View file

@ -118,6 +118,7 @@ func (cb *clipboard) Copy(ctx session.Context, req pb.RpcBlockCopyRequest) (text
if firstTextBlock != nil &&
req.SelectedTextRange != nil &&
!(req.SelectedTextRange.From == 0 && req.SelectedTextRange.To == 0) &&
!(req.SelectedTextRange.From == 0 && req.SelectedTextRange.To == int32(textutil.UTF16RuneCountString(firstTextBlock.GetText().Text))) &&
lastTextBlock == nil {
cutBlock, _, err := simple.New(firstTextBlock).(text.Block).RangeCut(req.SelectedTextRange.From, req.SelectedTextRange.To)
if err != nil {
@ -179,7 +180,8 @@ func (cb *clipboard) Cut(ctx session.Context, req pb.RpcBlockCutRequest) (textSl
if firstTextBlock != nil &&
lastTextBlock == nil &&
req.SelectedTextRange != nil &&
!(req.SelectedTextRange.From == 0 && req.SelectedTextRange.To == 0) {
!(req.SelectedTextRange.From == 0 && req.SelectedTextRange.To == 0) &&
!(req.SelectedTextRange.From == 0 && req.SelectedTextRange.To == int32(textutil.UTF16RuneCountString(firstTextBlock.GetText().Text))) {
first := s.Get(firstTextBlock.Id).(text.Block)
cutBlock, initialBlock, err := first.RangeCut(req.SelectedTextRange.From, req.SelectedTextRange.To)
@ -196,6 +198,7 @@ func (cb *clipboard) Cut(ctx session.Context, req pb.RpcBlockCutRequest) (textSl
}
}
cutBlock.GetText().Style = model.BlockContentText_Paragraph
textSlot = cutBlock.GetText().Text
anySlot = []*model.Block{cutBlock}
cbs := cb.blocksToState(req.Blocks)

View file

@ -4,6 +4,9 @@ import (
"strconv"
"testing"
"github.com/anyproto/anytype-heart/tests/blockbuilder"
"github.com/anyproto/anytype-heart/tests/testutil"
textutil "github.com/anyproto/anytype-heart/util/text"
"github.com/gogo/protobuf/types"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
@ -595,6 +598,16 @@ func TestClipboard_TitleOps(t *testing.T) {
}
}
requiredBlockReq := func(blockId string) *pb.RpcBlockPasteRequest {
return &pb.RpcBlockPasteRequest{
SelectedBlockIds: []string{blockId},
SelectedTextRange: &model.Range{},
AnySlot: []*model.Block{
newTextBlock("whatever").Model(),
},
}
}
multiBlockReq := &pb.RpcBlockPasteRequest{
FocusedBlockId: template.TitleBlockId,
SelectedTextRange: &model.Range{},
@ -612,8 +625,70 @@ func TestClipboard_TitleOps(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "single", st.Doc.Pick(template.TitleBlockId).Model().GetText().Text)
})
t.Run("single description to empty description", func(t *testing.T) {
for _, text := range []string{"", "full"} {
t.Run("paste - when ("+text+")", func(t *testing.T) {
// given
sb := smarttest.New("text")
require.NoError(t, smartblock.ObjectApplyTemplate(sb, nil, template.WithTitle))
sb.Doc = testutil.BuildStateFromAST(blockbuilder.Root(
blockbuilder.ID("root"),
blockbuilder.Children(
blockbuilder.Text(
text,
blockbuilder.ID("1"),
),
blockbuilder.Text(
"toggle",
blockbuilder.ID("2"),
blockbuilder.TextStyle(model.BlockContentText_Toggle),
),
)))
// when
cb := NewClipboard(sb, nil, nil, nil, nil)
_, _, _, _, err := cb.Paste(nil, &pb.RpcBlockPasteRequest{
FocusedBlockId: "1",
SelectedTextRange: &model.Range{From: 0, To: int32(textutil.UTF16RuneCountString(sb.Pick("1").Model().GetText().Text))},
AnySlot: []*model.Block{sb.Pick("2").Model()},
}, "")
// then
require.NoError(t, err)
assert.Equal(t, model.BlockContentText_Toggle, sb.Doc.Pick("1").Model().GetText().Style)
})
}
t.Run("paste - when insert partially", func(t *testing.T) {
// given
sb := smarttest.New("text")
sb.Doc = testutil.BuildStateFromAST(blockbuilder.Root(
blockbuilder.ID("root"),
blockbuilder.Children(
blockbuilder.Text(
"123",
blockbuilder.ID("1"),
),
blockbuilder.Text(
"toggle",
blockbuilder.ID("2"),
blockbuilder.TextStyle(model.BlockContentText_Toggle),
),
)))
// when
cb := NewClipboard(sb, nil, nil, nil, nil)
_, _, _, _, err := cb.Paste(nil, &pb.RpcBlockPasteRequest{
FocusedBlockId: "1",
SelectedTextRange: &model.Range{From: 1, To: 1},
AnySlot: []*model.Block{sb.Pick("2").Model()},
}, "")
// then
require.NoError(t, err)
assert.Equal(t, model.BlockContentText_Paragraph, sb.Pick("1").Model().GetText().Style)
})
t.Run("single description to empty title", func(t *testing.T) {
//given
state := withTitle(t, "")
addDescription(state, "current description")
cb := NewClipboard(state, nil, nil, nil, nil)
@ -632,6 +707,27 @@ func TestClipboard_TitleOps(t *testing.T) {
)
assert.True(t, true, find)
})
for _, blockIdToPasteTo := range []string{
template.TitleBlockId,
template.HeaderLayoutId,
template.FeaturedRelationsId,
template.DescriptionBlockId,
} {
t.Run("single text to "+blockIdToPasteTo, func(t *testing.T) {
//given
state := withTitle(t, "")
addRelations(state)
cb := NewClipboard(state, nil, nil, nil, nil)
//when
_, _, _, _, err := cb.Paste(nil, requiredBlockReq(blockIdToPasteTo), "")
//then
require.NoError(t, err)
assert.NotNil(t, state.Doc.Pick(blockIdToPasteTo))
})
}
t.Run("single to not empty title", func(t *testing.T) {
st := withTitle(t, "title")
cb := NewClipboard(st, nil, nil, nil, nil)
@ -816,6 +912,14 @@ func addDescription(st *smarttest.SmartTest, description string) {
state.ApplyState(newState, false)
}
func addRelations(st *smarttest.SmartTest) {
newState := st.Doc.NewState()
template.InitTemplate(newState, template.RequireHeader)
template.InitTemplate(newState, template.WithFeaturedRelations)
template.InitTemplate(newState, template.WithForcedDescription)
state.ApplyState(newState, false)
}
func TestClipboard_PasteToCodeBock(t *testing.T) {
sb := smarttest.New("text")
require.NoError(t, smartblock.ObjectApplyTemplate(sb, nil, template.WithTitle))
@ -917,6 +1021,68 @@ func Test_PasteText(t *testing.T) {
func Test_CopyAndCutText(t *testing.T) {
t.Run("copy/cut do not preserve style - when full text copied", func(t *testing.T) {
// given
sb := smarttest.New("text")
sb.Doc = testutil.BuildStateFromAST(blockbuilder.Root(
blockbuilder.ID("root"),
blockbuilder.Children(
blockbuilder.Text(
"toggle",
blockbuilder.ID("2"),
blockbuilder.TextStyle(model.BlockContentText_Toggle),
),
)))
// when
cb := NewClipboard(sb, nil, nil, nil, nil)
_, _, anySlotCopy, err := cb.Copy(pb.RpcBlockCopyRequest{
Blocks: []*model.Block{sb.Pick("2").Model()},
SelectedTextRange: &model.Range{From: 1, To: 1},
})
_, _, anySlotCut, err := cb.Cut(nil, pb.RpcBlockCutRequest{
SelectedTextRange: &model.Range{From: 1, To: 1},
Blocks: []*model.Block{sb.Pick("2").Model()},
})
// then
require.NoError(t, err)
assert.Equal(t, model.BlockContentText_Paragraph, anySlotCopy[0].GetText().Style)
assert.Equal(t, model.BlockContentText_Paragraph, anySlotCut[0].GetText().Style)
})
t.Run("copy/cut preserve style - when full text copied", func(t *testing.T) {
// given
sb := smarttest.New("text")
sb.Doc = testutil.BuildStateFromAST(blockbuilder.Root(
blockbuilder.ID("root"),
blockbuilder.Children(
blockbuilder.Text(
"toggle",
blockbuilder.ID("2"),
blockbuilder.TextStyle(model.BlockContentText_Toggle),
),
)))
// when
cb := NewClipboard(sb, nil, nil, nil, nil)
_, _, anySlotCopy, err := cb.Copy(pb.RpcBlockCopyRequest{
Blocks: []*model.Block{sb.Pick("2").Model()},
SelectedTextRange: &model.Range{From: 0, To: int32(textutil.UTF16RuneCountString(sb.Pick("2").Model().GetText().Text))},
})
_, _, anySlotCut, err := cb.Cut(nil, pb.RpcBlockCutRequest{
SelectedTextRange: &model.Range{From: 0, To: int32(textutil.UTF16RuneCountString(sb.Pick("2").Model().GetText().Text))},
Blocks: []*model.Block{sb.Pick("2").Model()},
})
// then
require.NoError(t, err)
assert.Equal(t, model.BlockContentText_Toggle, anySlotCopy[0].GetText().Style)
assert.Equal(t, model.BlockContentText_Toggle, anySlotCut[0].GetText().Style)
})
t.Run("copy/cut - when with children", func(t *testing.T) {
// given
sb := smarttest.New("text")

View file

@ -28,7 +28,7 @@ type pasteCtrl struct {
}
type pasteMode struct {
toTitle bool
toHeaderChild bool
removeSelection bool
multiRange bool
singleRange bool
@ -116,13 +116,10 @@ func (p *pasteCtrl) configure(req *pb.RpcBlockPasteRequest) (err error) {
return
}
selText := p.getFirstSelectedText()
p.mode.toTitle = selText != nil && p.s.HasParent(selText.Model().Id, template.HeaderLayoutId)
p.mode.intoBlockPasteStyle = p.mode.toTitle
p.mode.toHeaderChild = selText != nil && p.s.HasParent(selText.Model().Id, template.HeaderLayoutId)
p.mode.intoBlockPasteStyle = p.mode.toHeaderChild
if selText != nil && textCount == 1 && nonTextCount == 0 && req.IsPartOfBlock {
p.mode.intoBlock = true
if selText.GetText() == "" {
p.mode.intoBlockPasteStyle = true
}
} else {
p.mode.intoBlock = selText != nil && selText.Model().GetText().Style == model.BlockContentText_Code
}
@ -194,7 +191,8 @@ func (p *pasteCtrl) singleRange() (err error) {
return target.PasteInside(p.s, p.ps, secondBlock)
}
isPasteToHeader := targetId == template.TitleBlockId || targetId == template.DescriptionBlockId
isPasteToHeader := isRequiredRelation(targetId)
pos := model.Block_Bottom
if isPasteToHeader {
targetId = template.HeaderLayoutId
@ -274,7 +272,7 @@ func (p *pasteCtrl) insertUnderSelection() (err error) {
)
if len(p.selIds) > 0 {
targetId = p.selIds[0]
if targetId == template.TitleBlockId || targetId == template.DescriptionBlockId {
if isRequiredRelation(targetId) {
targetId = template.HeaderLayoutId
}
targetPos = model.Block_Bottom
@ -290,9 +288,18 @@ func (p *pasteCtrl) insertUnderSelection() (err error) {
})
}
func isRequiredRelation(targetID string) bool {
return targetID == template.TitleBlockId ||
targetID == template.DescriptionBlockId ||
targetID == template.FeaturedRelationsId ||
targetID == template.HeaderLayoutId
}
func (p *pasteCtrl) removeSelection() {
for _, toRemove := range p.selIds {
p.s.Unlink(toRemove)
if !isRequiredRelation(toRemove) {
p.s.Unlink(toRemove)
}
}
}

View file

@ -4,16 +4,15 @@ import (
"fmt"
"testing"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/block/simple/dataview"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
. "github.com/anyproto/anytype-heart/tests/blockbuilder"
)
@ -348,16 +347,6 @@ func TestState_ChangesCreate_MoveAdd_Side_NewBlock(t *testing.T) {
})
}
func buildStateFromAST(root *Block) *State {
st := NewDocFromSnapshot("", &pb.ChangeSnapshot{
Data: &model.SmartBlockSnapshotBase{
Blocks: root.Build(),
},
}).(*State)
ApplyState(st, true)
return st.NewState()
}
func TestState_SetParent(t *testing.T) {
orig := NewDoc("root", nil).(*State)
orig.Add(simple.New(&model.Block{Id: "root", ChildrenIds: []string{"header"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
@ -763,3 +752,14 @@ func newRemoveChange(ids ...string) *pb.ChangeContent {
},
}
}
// copy if testutil.BuildStateFromAST because of cyclic import
func buildStateFromAST(root *Block) *State {
st := NewDocFromSnapshot("", &pb.ChangeSnapshot{
Data: &model.SmartBlockSnapshotBase{
Blocks: root.Build(),
},
}).(*State)
ApplyState(st, true)
return st.NewState()
}

View file

@ -303,17 +303,18 @@ func (t *Text) RangeTextPaste(rangeFrom int32, rangeTo int32, copiedBlock *model
copyFrom := int32(0)
copyTo := int32(textutil.UTF16RuneCountString(copiedText.Text))
if rangeFrom < 0 || int(rangeFrom) > textutil.UTF16RuneCountString(t.content.Text) {
textLen := textutil.UTF16RuneCountString(t.content.Text)
if rangeFrom < 0 || int(rangeFrom) > textLen {
return caretPosition, fmt.Errorf("out of range: range.from is not correct: %d", rangeFrom)
}
if rangeTo < 0 || int(rangeTo) > textutil.UTF16RuneCountString(t.content.Text) {
if rangeTo < 0 || int(rangeTo) > textLen {
return caretPosition, fmt.Errorf("out of range: range.to is not correct: %d", rangeTo)
}
if rangeFrom > rangeTo {
return caretPosition, fmt.Errorf("out of range: range.from %d > range.to %d", rangeFrom, rangeTo)
}
if len(t.content.Text) == 0 || (rangeFrom == 0 && rangeTo == int32(len(t.content.Text))) {
if textLen == 0 || (rangeFrom == 0 && rangeTo == int32(textLen)) {
if !isPartOfBlock {
t.content.Style = copiedText.Style
t.content.Color = copiedText.Color

7
go.mod
View file

@ -80,14 +80,14 @@ require (
github.com/yuin/goldmark v1.5.6
go.uber.org/mock v0.2.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.25.0
go.uber.org/zap v1.26.0
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
golang.org/x/image v0.12.0
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda
golang.org/x/net v0.15.0
golang.org/x/oauth2 v0.12.0
golang.org/x/text v0.13.0
google.golang.org/grpc v1.57.0
google.golang.org/grpc v1.58.1
gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20180125164251-1832d8546a9f
gopkg.in/yaml.v3 v3.0.1
storj.io/drpc v0.0.33
@ -253,6 +253,9 @@ require (
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

18
tests/testutil/state.go Normal file
View file

@ -0,0 +1,18 @@
package testutil
import (
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/tests/blockbuilder"
)
func BuildStateFromAST(root *blockbuilder.Block) *state.State {
st := state.NewDocFromSnapshot("", &pb.ChangeSnapshot{
Data: &model.SmartBlockSnapshotBase{
Blocks: root.Build(),
},
}).(*state.State)
state.ApplyState(st, true)
return st.NewState()
}