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:
commit
4d7c2f7f54
10 changed files with 230 additions and 32 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -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
|
||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
7
go.mod
|
@ -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
18
tests/testutil/state.go
Normal 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()
|
||||
}
|
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue