Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iterator peek next #26

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

cgestes
Copy link
Contributor

@cgestes cgestes commented Oct 21, 2021

Please review. Some changes may be better done in another way.

This PR is needed to support Markdown auto formatting rules in Notus.

lib/quill_delta.dart Outdated Show resolved Hide resolved
lib/quill_delta.dart Show resolved Hide resolved
int _docIndex = 0; // index in the current document

// Makes an independent copy of this iterator
DeltaIterator clone() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clone and resetTo are needed to be able to store a position or go back in time.

@cgestes cgestes force-pushed the iterator-peek-next branch 3 times, most recently from 50bfece to 03f7c8b Compare October 22, 2021 02:11
_docIndex = it._docIndex;
}

static Operation end() => Operation.retain(maxLength);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this just be null or Operation.retain(-1) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or anything that Operation can express as invalid

final opActualLength = opIsNotEmpty ? opLength : actualLength;
return Operation._(opKey, opActualLength, opData, opAttributes);
if (_index >= delta.length) {
return end();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it used to return Operation.retain(length) which sound like a hack to me.

can we return null or Operation.retain(-1). I mean anything invalid... (I understand null is probably very inpractical)

Zefyr/Notus needs very little adjustment after this changes. (4 lines needs to be changed) (and they become more understandable)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea! I'm thinking we can use Operation.insert with a special data value, e.g. create constant and use it:

class _EOF {
  const _EOF();
}
const _eof = _EOF();

// Then in the iterator
return Operation.insert('insert', 1, _eof);

Can also add Operation.isEof to allow easy checking for this.

That said, I'd prefer for this change to be a separate PR, if you don't mind. There is already a lot of new changes here which makes it hard to review.

Copy link
Owner

@pulyaevskiy pulyaevskiy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks this looks impressive!

It took me a while to digest everything here but I have a couple of concerns about the new APIs introduced by this change (specifically rewind is something I'd prefer to avoid).

I also believe we can achieve similar results with a bit simpler solution. It'd only require one additional method on the iterator - peek, which is similar to peekLength but returns the actual operation instead of just length. E.g.:

  Operation peek() {
    if (_index < delta.length) {
      final op = delta._operations[_index];
      if (_offset == 0) return op;
      return Operation._(op.key, op.length, op.data, op.attributes);
    }
    return Operation.retain(maxLength);
  }

Next we can implement both skipToLineAtIndex and skipToEndOfLine using existing methods of the iterator. One important change is that I suggest tweak the return type of these functions.

A semi-pseudo code of both of these is below:

/// Skips [length] characters in source delta and returns contents of the last
/// line up until [length].
/// The returned list of operations can be used to compose the text prefix pretty easily
///
/// Only makes sense to use on a document delta (containing only insert operations).
List<Operation> skipToLineAt(DeltaIterator iter, int length) {
  assert(length > 0);

  final prefix = <Operation>[];

  var skipped = 0;
  while (skipped < length && iter.hasNext) {
    final opLength = iter.peekLength();
    final skip = math.min(length - skipped, opLength);
    final op = iter.next(skip);
    if (op.data is! String) {
      prefix.add(op);
    } else {
      var text = op.data as String;
      var pos = text.lastIndexOf('\n');
      if (pos == -1) {
        prefix.add(op);
      } else {
        prefix.clear();
        prefix.add(Operation.insert(text.substring(pos + 1), op.attributes));
      }
    }
    skipped += op.length;
  }
  return prefix;
}

/// Skips to the end of current line and returns all skipped operations
/// including operation containing the newline character.
/// The returned list of operations can be used to find the total skipped length
///
/// Only makes sense to use on a document delta (containing only insert operations).
List<Operation> skipToEndOfLine(DeltaIterator iter) {
  assert(iter.hasNext);

  final result = <Operation>[];
  while (iter.hasNext) {
    final op = iter.peek();
    if (op.data is! String) {
      result.add(op);
      iter.next();
      continue;
    }
    final text = op.data as String;
    final pos = text.indexOf('\n');
    if (pos == -1) {
      result.add(op);
      iter.next();
      continue;
    }
    result.add(iter.next(pos + 1));
  }
  return result;
}

I still think that these two functions better to keep in Zefyr as these have a narrower use and only make sense for document deltas (only insert ops).

Sorry for dragging you into a different direction here, hope it makes sense. WDYT?

_DeltaIteratorState(
{required this.index, required this.offset, required this.docIndex});
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be used anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over from implementing peekNext 😅

final opActualLength = opIsNotEmpty ? opLength : actualLength;
return Operation._(opKey, opActualLength, opData, opAttributes);
if (_index >= delta.length) {
return end();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea! I'm thinking we can use Operation.insert with a special data value, e.g. create constant and use it:

class _EOF {
  const _EOF();
}
const _eof = _EOF();

// Then in the iterator
return Operation.insert('insert', 1, _eof);

Can also add Operation.isEof to allow easy checking for this.

That said, I'd prefer for this change to be a separate PR, if you don't mind. There is already a lot of new changes here which makes it hard to review.

@cgestes
Copy link
Contributor Author

cgestes commented Oct 22, 2021

I can move the extension in Notus. No issue. I believe they should just exist somewhere cause they are a pain in the ass to write.

I did implement peek before but it makes the code that uses it hard to grok.

And peek now is:

Final newIter = iter.clone()
newIter.next()

Also I believe skipToBeginningOfLineBeforeIndex would be more optimised if we skip to index and then look backward. Otherwise we look for \n on every single line from start of doc.

@cgestes
Copy link
Contributor Author

cgestes commented Oct 22, 2021

For EOF I like the idea too. Will implement in a future PR.
I came to it cause the doc on DeltaItarator.next was incorrect, so I reimplement by mistake what the doc was saying...then had to fix the test in Notus and noticed it was all making more sense 😅

@cgestes cgestes force-pushed the iterator-peek-next branch 2 times, most recently from acd5047 to b8c13d5 Compare October 22, 2021 13:51
@@ -652,19 +656,17 @@ class Delta {

/// Returns slice of this delta from [start] index (inclusive) to [end]
/// (exclusive).
Delta slice(int start, [int? end]) {
Delta slice(int start, [int end = DeltaIterator.maxLength]) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just simplification of the code. no logic changes

@codecov
Copy link

codecov bot commented Oct 22, 2021

Codecov Report

Merging #26 (b8c13d5) into master (4846633) will decrease coverage by 10.48%.
The diff coverage is 43.75%.

❗ Current head b8c13d5 differs from pull request most recent head c950b1b. Consider uploading reports for the commit c950b1b to get more accurate results
Impacted file tree graph

@@             Coverage Diff             @@
##           master      #26       +/-   ##
===========================================
- Coverage   93.56%   83.08%   -10.49%     
===========================================
  Files           1        1               
  Lines         342      396       +54     
===========================================
+ Hits          320      329        +9     
- Misses         22       67       +45     
Impacted Files Coverage Δ
lib/quill_delta.dart 83.08% <43.75%> (-10.49%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4846633...c950b1b. Read the comment docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants