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

Apply schema to model #1

Merged
merged 1 commit into from
Oct 19, 2024
Merged

Apply schema to model #1

merged 1 commit into from
Oct 19, 2024

Conversation

hackerwins
Copy link
Member

@hackerwins hackerwins commented Oct 12, 2024

Apply schema to model

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a new command structure for text manipulation, allowing for operations like text insertion and block splitting.
    • Enhanced the Editor class to support schema management and improved text insertion logic.
    • Added new methods for handling node positions and operations, improving the model's functionality.
    • Implemented utility functions for array manipulation, enhancing data handling capabilities.
    • Added a new schema for plain text documents, improving XML conversion capabilities.
    • New functionality for retrieving and converting DOM selections into structured ranges.
  • Bug Fixes

    • Updated the Devtools plugin to display the model's value in XML format instead of plain text.
  • Tests

    • Added comprehensive unit tests for the Model, Nodes, and utility functions to ensure reliability and correctness.
    • Introduced new tests for the pathOf function and enhanced existing tests for the offsetOf function.

@hackerwins hackerwins marked this pull request as draft October 14, 2024 00:05
@hackerwins hackerwins marked this pull request as ready for review October 19, 2024 08:08
@hackerwins hackerwins marked this pull request as draft October 19, 2024 08:09
@hackerwins hackerwins marked this pull request as ready for review October 19, 2024 08:09
Copy link

coderabbitai bot commented Oct 19, 2024

Walkthrough

The changes involve significant restructuring and enhancements across multiple files in the codebase. The src/commands.ts file was deleted, and a new command structure was introduced in src/commands/commands.ts, including functions for text manipulation. The Editor class in src/editor.ts was updated to integrate schema management and improve text insertion logic. The Model class in src/model/model.ts transitioned to a more complex structure using operations, while several utility functions and types were added across various files to facilitate node and range management. New tests were also introduced to ensure functionality.

Changes

File Change Summary
src/commands.ts Deleted; contained command definitions and an execute function.
src/commands/commands.ts Added new command structure with Command type, insertText, splitBlock, and execute functions for text manipulation.
src/editor.ts Updated EditorOptions to include schema, modified create method, adjusted Model instantiation, and changed insertText logic to use new command functions.
src/main.ts Updated initialValue to an HTML string and introduced a new schema object for document structure.
src/model/model.ts Transitioned to an operation-based model with schema validation; added methods for creating nodes, applying operations, and managing content structure.
src/model/node.ts Deleted; contained type definitions for node structures.
src/model/nodepos.ts Introduced functions isLeftMost, isRightMost, and equals for node position management.
src/model/nodes.ts Added utility functions for node manipulation, including toXML, toNodePos, pathOf, and node insertion/removal functions.
src/model/operations.ts Introduced Operation, EditOperation, and MoveOperation types for editing and moving content.
src/model/range.ts Added isCollapsed function to determine if a range is collapsed.
src/model/schema.ts Updated type definitions, added BasicSchema, and introduced methods for handling nodes from various formats (DOM, JSON, XML).
src/model/types.ts Added types for Node, Element, Text, Path, Range, NodePos, and IndexRange to establish a foundational type system.
src/plugins/devtools.ts Updated initialize method to display model value in XML format instead of plain text.
src/range.ts Deleted; contained a type definition for Range.
src/utils/array.ts Added utility functions for array manipulation: firstOf, lastOf, initialOf, tailOf, groupBy, and takeWhile.
src/view/selection.ts Introduced Position type and functions getSelection, toRange, and pathOf; renamed existing toRange function to toIndexRange.
src/view/view.ts Updated handleBeforeInput method to utilize new command functions and added getSelection method.
test/model/helper.ts Introduced PlainTextSpec schema and tokenToXML function for XML conversion.
test/model/model.test.ts Added unit tests for Model class, verifying creation and editing functionalities.
test/model/nodes.test.ts Introduced unit tests for node manipulation functions, including toNodePos, nodesBetween, and serialization methods.
test/model/schema.test.ts Added test case for building a node from XML input in the Schema class.
test/util/array.test.ts Introduced unit tests for groupBy and takeWhile utility functions.
test/view/selection.test.ts Added tests for the pathOf function and updated tests for offsetOf function for consistency.

Poem

🐇 In the garden of code, changes bloom bright,
Commands now dance, in structured delight.
Models and schemas, a wonderful weave,
With nodes and operations, we joyfully achieve.
Text flows like rivers, in XML streams,
A world of creation, born from our dreams! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Outside diff range and nitpick comments (36)
src/model/range.ts (2)

2-4: Consider enhancing the function documentation.

While the current documentation is clear, it could be more informative. Consider adding details about the parameter and return value.

Here's a suggested improvement:

/**
 * Checks if the given range is collapsed.
 * @param range The Range object to check.
 * @returns {boolean} True if the range is collapsed, false otherwise.
 */

1-8: Overall, good implementation with room for minor improvements.

The isCollapsed function is well-implemented and efficient. Consider the suggested improvements for documentation and edge case handling. Additionally, it would be beneficial to add unit tests for this function to ensure it behaves correctly for various input scenarios.

Would you like assistance in generating unit tests for the isCollapsed function?

src/main.ts (1)

10-14: Well-structured schema introduction

The introduction of the schema object is a significant improvement. It defines a clear structure for the document, allowing for better content organization and potentially richer text formatting. The schema aligns well with common HTML structures, which is good for compatibility and familiarity.

Consider expanding the schema in the future to support more complex document structures, such as headings, lists, or inline formatting elements (bold, italic, etc.). This would allow for even richer content creation capabilities.

src/model/operations.ts (2)

9-17: LGTM with a minor suggestion: Update JSDoc comment for consistency.

The EditOperation type is well-structured and appropriate for representing an edit operation in the editor. However, there's a small inconsistency in the JSDoc comment.

Consider updating the JSDoc comment to use "EditOperation" instead of "ReplaceOperation" for consistency:

 /**
- * `ReplaceOperation` represents a operation that replaces a range of nodes with
+ * `EditOperation` represents an operation that replaces a range of nodes with
  * another range of nodes.
  */

19-27: LGTM with a minor suggestion: Correct grammar in JSDoc comment.

The MoveOperation type is well-defined and appropriate for representing a move operation in the editor. The structure and naming are clear and consistent.

There's a minor grammatical issue in the JSDoc comment. Consider updating it as follows:

 /**
- * `MoveOperation` represents a operation that moves a range of nodes to another
+ * `MoveOperation` represents an operation that moves a range of nodes to another
  * position.
  */
test/model/helper.ts (1)

14-26: LGTM: tokenToXML function is well-implemented. Consider enhancing the JSDoc.

The tokenToXML function is logically sound and handles all cases of the TagType enum. The implementation is concise and effective.

Consider enhancing the JSDoc comment to include information about the parameters and return value. For example:

/**
 * Converts a node and tag type to an XML string.
 * @param node - The node to convert.
 * @param type - The type of tag to generate ('open', 'close', or 'text').
 * @returns The XML string representation of the node.
 */
test/model/model.test.ts (2)

6-10: LGTM: Good test for model creation, consider adding more assertions.

This test case effectively verifies that a Model can be created with a schema and initial value. The use of a template literal for the XML improves readability.

Consider adding assertions to check other properties of the created model, such as its schema, to ensure it's fully initialized as expected.


1-23: Overall assessment: Good start on Model tests, room for expansion

This new test file for the Model class provides a solid foundation for testing core functionalities. The tests are well-structured and follow best practices. However, there are opportunities for improvement:

  1. Expand test coverage to include more scenarios for text editing.
  2. Address the TODO for testing inverse operations.
  3. Consider adding tests for edge cases and potential error conditions.

These additions will enhance the robustness of the test suite and ensure the Model class behaves correctly under various conditions.

As the Model class evolves, remember to update and expand these tests accordingly to maintain comprehensive coverage.

src/model/nodepos.ts (3)

4-27: LGTM with a minor suggestion: Enhance JSDoc comment.

The isOutOfRange function is well-implemented with correct logic and good use of early returns. However, the JSDoc comment could be more descriptive.

Consider expanding the JSDoc comment to provide more details about the function's parameters and return value. For example:

/**
 * Checks if the given node is out of the range defined by start and end positions.
 * @param node - The node to check.
 * @param start - The start position of the range.
 * @param end - The end position of the range.
 * @returns {boolean} True if the node is out of range, false otherwise.
 */

29-34: LGTM with a minor suggestion: Enhance JSDoc comment.

The isLeftMost function is correctly implemented. However, the JSDoc comment could be more informative.

Consider expanding the JSDoc comment to provide more details about the function's parameter and return value. For example:

/**
 * Checks if the given node position is at the leftmost position of the node.
 * @param pos - The node position to check.
 * @returns {boolean} True if the position is at the leftmost (offset 0), false otherwise.
 */

36-41: LGTM with a minor suggestion: Enhance JSDoc comment.

The isRightMost function is correctly implemented, using the lengthOf function to determine if the position is at the rightmost edge of the node. However, the JSDoc comment could be more informative.

Consider expanding the JSDoc comment to provide more details about the function's parameter and return value. For example:

/**
 * Checks if the given node position is at the rightmost position of the node.
 * @param pos - The node position to check.
 * @returns {boolean} True if the position is at the rightmost (offset equals node length), false otherwise.
 */
test/model/schema.test.ts (2)

52-65: LGTM: Comprehensive test for fromXML method.

The new test case effectively covers the functionality of the fromXML method, including both successful conversion and error handling. It aligns well with the existing test structure and provides good coverage.

Consider adding more test scenarios to cover edge cases:

  1. Test with nested elements.
  2. Test with attributes in the XML.
  3. Test with empty elements.

Example:

it('should handle complex XML structures', () => {
  const schema = new Schema({
    root: { children: 'p*' },
    p: { children: '(text|span)*', attrs: { id: {} } },
    span: { children: 'text*' },
    text: {},
  });

  const val = /*html*/ `<p id="test"><span>Hello</span>, <span>world!</span></p>`;
  expect(toXML(schema.fromXML(val))).toEqual(val);

  const emptyVal = /*html*/ `<p></p>`;
  expect(toXML(schema.fromXML(emptyVal))).toEqual(emptyVal);
});

Line range hint 1-65: Overall assessment: Positive addition to the test suite.

The new test case for fromXML method enhances the coverage of the Schema class functionality. It maintains consistency with existing tests and introduces valuable checks for XML conversion. The changes are well-integrated and improve the overall quality of the test suite.

As the Schema class evolves, consider organizing tests into describe blocks for each method (e.g., describe('fromJSON'), describe('fromXML')). This structure will improve readability and make it easier to add more specific test cases in the future.

src/model/types.ts (3)

6-14: LGTM: Element type definition is comprehensive. Consider using a non-optional children array.

The Element type definition is well-structured and covers all necessary properties for an element node. However, consider making the children property non-optional with a default empty array. This can simplify operations on elements by ensuring that children is always an array.

Consider updating the children property as follows:

children: Array<Node> = [];

This change would ensure that children is always initialized as an empty array, potentially simplifying operations on elements.


36-42: LGTM: Range type definition is concise. Consider adding more detailed documentation.

The Range type effectively represents a range in the editor content using Path for both start and end. This ensures consistency with the Path type.

Consider expanding the comment to include an example, similar to the Path type documentation. This would further clarify how Range is used in practice.


44-51: LGTM: NodePos type definition is clear. Consider adding a conversion method.

The NodePos type effectively represents a node's position in the model. The comment suggests that it can be converted to Path, which is a useful feature.

Consider adding a utility function or method to convert NodePos to Path. This would make the conversion process explicit and easily accessible. For example:

export function nodePosToPatch(nodePos: NodePos): Path {
  // Implementation details
}
test/util/array.test.ts (2)

4-32: LGTM: Comprehensive tests for groupBy, with a suggestion for improvement.

The test cases for groupBy are well-structured and cover various scenarios, including edge cases. They effectively test the function's behavior for different inputs and grouping conditions.

Consider adding a test case for non-primitive types or custom comparison functions to further improve coverage. For example:

it('should group objects by a custom comparison function', () => {
  const objects = [
    { id: 1, category: 'A' },
    { id: 2, category: 'A' },
    { id: 3, category: 'B' },
    { id: 4, category: 'B' },
  ];
  const result = groupBy(objects, (prev, curr) => prev.category === curr.category);
  expect(result).toEqual([
    [{ id: 1, category: 'A' }, { id: 2, category: 'A' }],
    [{ id: 3, category: 'B' }, { id: 4, category: 'B' }],
  ]);
});

34-52: LGTM: Well-structured tests for takeWhile, with a suggestion for improvement.

The test cases for takeWhile are clear and cover the main scenarios, including edge cases like empty input and always true predicate.

Consider adding a test case where the first element doesn't satisfy the condition, resulting in an empty array. This would ensure the function correctly handles immediate termination. For example:

it('should return an empty array when the first element does not satisfy the condition', () => {
  const array = [10, 1, 2, 3, 4, 5];
  const result = takeWhile(array, (v) => v < 5);
  expect(result).toEqual([]);
});
src/utils/array.ts (4)

17-22: LGTM! Consider enhancing the JSDoc comment.

The initialOf function is correctly implemented and type-safe. The logic to return all elements except the last one is accurate.

Consider enhancing the JSDoc comment to mention the behavior for empty arrays:

/**
 * `initialOf` returns all elements of an array except the last one.
 * If the array is empty, it returns an empty array.
 */

24-29: LGTM! Consider enhancing the JSDoc comment.

The tailOf function is correctly implemented and type-safe. The logic to return all elements except the first one is accurate.

Consider enhancing the JSDoc comment to mention the behavior for empty arrays, similar to the suggestion for initialOf:

/**
 * `tailOf` returns all elements of an array except the first one.
 * If the array is empty, it returns an empty array.
 */

31-55: LGTM! Consider enhancing documentation and simplifying implementation.

The groupBy function is correctly implemented and type-safe. The logic to group adjacent elements is accurate.

  1. Enhance the JSDoc comment to provide more details about the function's behavior:
/**
 * `groupBy` groups adjacent elements of an array according to a given function.
 * @param array - The input array to be grouped.
 * @param fn - A function that takes two adjacent elements and returns true if they should be in the same group.
 * @returns An array of arrays, where each inner array is a group of adjacent elements.
 * @example
 * groupBy([1, 2, 2, 3, 4, 4, 5], (a, b) => a === b)
 * // Returns [[1], [2, 2], [3], [4, 4], [5]]
 */
  1. Consider simplifying the implementation for better readability:
export function groupBy<T>(
  array: Array<T>,
  fn: (prev: T, curr: T) => boolean,
): Array<Array<T>> {
  return array.reduce((groups: Array<Array<T>>, current: T) => {
    const lastGroup = groups[groups.length - 1];
    if (lastGroup && fn(lastGroup[lastGroup.length - 1], current)) {
      lastGroup.push(current);
    } else {
      groups.push([current]);
    }
    return groups;
  }, []);
}

This implementation eliminates the need for separate handling of the first element and simplifies the logic.


57-73: LGTM! Complete the JSDoc comment.

The takeWhile function is correctly implemented and type-safe. The logic to take elements while the predicate function returns true is accurate and efficient.

Complete the JSDoc comment to fully describe the function's behavior:

/**
 * `takeWhile` takes elements from the beginning of an array while the given
 * predicate function returns true.
 * @param array - The input array.
 * @param fn - A predicate function that takes an element and returns a boolean.
 * @returns A new array containing elements from the beginning of the input array
 *          until the predicate function returns false.
 */
test/view/selection.test.ts (4)

13-13: Good use of template literal for HTML content.

The change to use a template literal with the html comment improves readability and enables better syntax highlighting in IDEs. This is a good practice.

Consider applying this change consistently to all HTML strings in the file for uniformity.


29-58: Well-structured test suite for pathOf function.

The new test suite for pathOf is comprehensive and mirrors the structure of the existing offsetOf tests. It covers various scenarios including nested elements and error cases, providing good coverage for the new function.

Consider adding a test case for an empty element (e.g., <br>) to ensure the function handles all possible DOM structures correctly.


30-30: Consider improving test descriptions for clarity.

While the consistency in test descriptions between offsetOf and pathOf is good, they could be more specific to each function's purpose. For example:

  • "should convert DOM position to offset" for offsetOf
  • "should convert DOM position to path" for pathOf

This would make the purpose of each test suite clearer at a glance.

Also applies to: 36-36, 51-51


Line range hint 1-58: Good overall test structure and coverage.

The parallel structure between offsetOf and pathOf tests is excellent for consistency and readability. The test coverage appears comprehensive, including error cases for both functions.

Consider adding the following test cases to further improve coverage:

  1. Test with a deeply nested DOM structure to ensure correct handling of complex paths.
  2. Test with text nodes that contain special characters or emojis.
  3. Test the behavior when dealing with comment nodes.
src/view/view.ts (2)

53-54: Improved input handling implementation.

The changes to handleBeforeInput simplify the code and align well with the new command structure. This is a good improvement.

Consider adding error handling or logging for unexpected scenarios. For example:

if (CommonInputEventTypes.includes(event.inputType)) {
  const range = toRange(event.getTargetRanges()[0], this.container);
  const text = this.getTextFromEvent(event);
  try {
    this.notify(insertText(range, text));
  } catch (error) {
    console.error('Error in handleBeforeInput:', error);
    // Optionally, add more specific error handling here
  }
}

This would help with debugging and make the code more robust.

Also applies to: 59-60


80-82: New getSelection method added.

The new getSelection method is a good addition, providing a way to retrieve the current selection from the container.

Consider adding a brief JSDoc comment to describe the method's purpose and return value. For example:

/**
 * Retrieves the current selection from the container.
 * @returns {Range | undefined} The current selection range, or undefined if no selection exists.
 */
getSelection(): Range | undefined {
  return getSelection(this.container);
}

This would improve the code's self-documentation and make it easier for other developers to understand and use the method.

test/model/nodes.test.ts (2)

33-70: LGTM: 'Nodes.Between' tests are thorough and well-implemented.

The 'Nodes.Between' describe block contains three comprehensive test cases that cover important scenarios:

  1. Iterating over nodes between two positions
  2. Iterating over a text node
  3. Handling the case when start and end positions are the same

All tests use clear XML examples and appropriate assertions to verify the expected behavior.

Consider extracting the common logic for converting nodes to XML into a helper function to improve code reusability. For example:

function nodesToXML(container: Element, start: Pos, end: Pos): string {
  return Array.from(nodesBetween(container, start, end))
    .map(([node, type]) => tokenToXML(node, type))
    .join('');
}

This function could then be used in all three test cases, reducing duplication and improving maintainability.


72-100: LGTM: 'Nodes' tests cover essential node manipulation operations.

The 'Nodes' describe block contains three well-implemented test cases that cover important node manipulation operations:

  1. Serializing to XML
  2. Splitting a text node
  3. Inserting a node after another and removing a node

All tests use clear XML examples and appropriate assertions to verify the expected behavior.

Consider splitting the third test case (lines 87-99) into two separate test cases for better test isolation:

  1. A test case for inserting a node after another
  2. A test case for removing a node

This separation will make the tests more focused and easier to maintain. For example:

it('should insert a node after another', () => {
  const val = /*html*/ `<p>Hello</p>`;
  const container = plainTextSchema.fromXML(val) as Element;
  const node = insertAfter(container.children![0], {
    type: 'text',
    text: ', world!',
  })[0];
  expect(toXML(node!)).toEqual(', world!');
  expect(toXML(container)).toEqual(/*html*/ `<p>Hello, world!</p>`);
});

it('should remove a node', () => {
  const val = /*html*/ `<p>Hello, world!</p>`;
  const container = plainTextSchema.fromXML(val) as Element;
  removeNode(container.children![0]);
  expect(toXML(container)).toEqual(/*html*/ `<p></p>`);
});

This approach will make it easier to identify which specific operation might be failing if a test breaks in the future.

src/commands/commands.ts (2)

38-38: Specify the type of the ops array for clarity

For better type safety and code readability, consider specifying the type of the ops array when initializing it. This makes the code more explicit and helps prevent potential type-related issues.

Apply this diff:

-  const ops = [];
+  const ops: Operation[] = [];

45-47: Use object property shorthand for ops

Since the property name and the variable name are the same, you can use the object property shorthand syntax for brevity.

Apply this diff:

 return {
-  ops: ops,
+  ops,
 };
src/view/selection.ts (1)

118-121: Clarify the Purpose of toIndexRange Function

The toIndexRange function converts an AbstractRange to an IndexRange using the offsetOf function. Since there are both toRange and toIndexRange functions, please ensure that their purposes are clearly differentiated.

Consider enhancing the documentation comments for both functions to explain their distinct roles and how they should be used, preventing potential confusion for future maintainers.

src/model/model.ts (1)

95-95: Rename 'withSplitText' parameter for clarity

The parameter name withSplitText could be more concise. Renaming it to splitText improves readability and better reflects its purpose.

Apply this diff to rename the parameter:

- nodePosOf(path: Array<number>, withSplitText: boolean = true): NodePos {
+ nodePosOf(path: Array<number>, splitText: boolean = true): NodePos {

And update the reference inside the method:

- if (!withSplitText) {
+ if (!splitText) {

Also applies to: 97-97

src/model/nodes.ts (2)

114-131: Handle return value in splitText function

The splitText function returns a new Text node when a split occurs, but it doesn't handle the case where no split is performed (i.e., when it returns undefined). Ensure that the calling code properly handles the undefined case to prevent potential runtime errors.


172-185: Ensure proper cleanup in removeNode

After removing the node from parent.children, the node.parent reference still points to the old parent. Consider setting node.parent to undefined to fully detach the node.

Apply this diff to update the function:

  children.splice(index, 1);
+ node.parent = undefined;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 79151f7 and 2813043.

📒 Files selected for processing (23)
  • src/commands.ts (0 hunks)
  • src/commands/commands.ts (1 hunks)
  • src/editor.ts (6 hunks)
  • src/main.ts (1 hunks)
  • src/model/model.ts (1 hunks)
  • src/model/node.ts (0 hunks)
  • src/model/nodepos.ts (1 hunks)
  • src/model/nodes.ts (1 hunks)
  • src/model/operations.ts (1 hunks)
  • src/model/range.ts (1 hunks)
  • src/model/schema.ts (4 hunks)
  • src/model/types.ts (1 hunks)
  • src/plugins/devtools.ts (1 hunks)
  • src/range.ts (0 hunks)
  • src/utils/array.ts (1 hunks)
  • src/view/selection.ts (2 hunks)
  • src/view/view.ts (3 hunks)
  • test/model/helper.ts (1 hunks)
  • test/model/model.test.ts (1 hunks)
  • test/model/nodes.test.ts (1 hunks)
  • test/model/schema.test.ts (2 hunks)
  • test/util/array.test.ts (1 hunks)
  • test/view/selection.test.ts (3 hunks)
💤 Files with no reviewable changes (3)
  • src/commands.ts
  • src/model/node.ts
  • src/range.ts
🧰 Additional context used
🪛 Biome
src/model/schema.ts

[error] 9-9: Don't use '{}' as a type.

Prefer explicitly define the object shape. '{}' means "any non-nullable value".

(lint/complexity/noBannedTypes)

🔇 Additional comments (47)
src/model/range.ts (2)

1-1: LGTM: Import statement is correct.

The import statement correctly imports the Range type from a local types file, which is a good practice for type management and code organization.


5-8: LGTM: Function implementation is correct and efficient.

The isCollapsed function correctly checks if a range is collapsed by comparing the length and elements of the start and end arrays. The use of Array.every method is an efficient way to compare array elements.

src/main.ts (2)

9-9: Improved initial value structure

The change from a plain string to an HTML string ('<p>Hello,</p>') for the initialValue is a good improvement. This structured format aligns well with the newly introduced schema and provides better semantic meaning to the initial content.


9-14: Comprehensive update enhancing editor structure

The changes in this file, particularly the introduction of structured initial content and a schema, represent a significant step towards a more robust and flexible editor. These modifications align well with the PR objective of applying a schema to the model.

To ensure consistency across the codebase, let's verify the usage of the updated Editor.create method:

This will help us confirm that all calls to Editor.create have been updated to include the new schema parameter.

✅ Verification successful

Verified: Editor.create usage is consistent across the codebase

All instances of Editor.create have been updated to include the schema parameter, ensuring consistency and alignment with the PR objectives.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the usage of Editor.create method across the codebase

# Test: Search for Editor.create usage
rg --type typescript "Editor\.create" -A 5

Length of output: 79


Script:

#!/bin/bash
# Description: Verify the usage of Editor.create method across the codebase using correct file type

# Test: Search for Editor.create usage in TypeScript files
rg --type ts "Editor\.create" -A 5

Length of output: 277

src/model/operations.ts (3)

1-1: LGTM: Imports are concise and specific.

The import statement is well-structured, importing only the necessary types (Node and Range) from the local types module. This approach promotes clean and efficient code organization.


3-7: LGTM: Well-defined Operation type with clear documentation.

The Operation type is well-defined as a union of EditOperation and MoveOperation. The accompanying JSDoc comment clearly explains its purpose, which is crucial for maintaining readable and self-documenting code.


1-27: LGTM: Well-structured and consistent type definitions.

The overall structure of this file is excellent. It follows good TypeScript practices with clear, well-documented type definitions. The use of union types for Operation and specific types for EditOperation and MoveOperation allows for flexible and type-safe operations in the editor.

The file maintains a single focus on defining operation types, which is good for modularity and separation of concerns. This structure will make it easier to maintain and extend the operations in the future.

test/model/helper.ts (4)

1-3: LGTM: Import statements are appropriate.

The import statements are concise and relevant to the file's purpose. They correctly import the necessary types and classes from both the source and the current directory.


5-12: LGTM: PlainTextSpec is well-defined and documented.

The PlainTextSpec constant provides a clear and logical schema for a plain text document. The structure with a root element containing paragraphs, which in turn contain text nodes, is appropriate. The JSDoc comment enhances code readability.


28-28: LGTM: plainTextSchema is correctly instantiated.

The plainTextSchema constant is properly created using the Schema class and the previously defined PlainTextSpec. This provides a convenient, ready-to-use schema instance for plain text documents in tests.


1-28: Overall, excellent implementation of the test helper file.

This new file, test/model/helper.ts, is well-structured and serves its purpose effectively. It introduces a clear schema for plain text documents and provides useful utility functions for testing. The code is clean, follows good practices, and is appropriately documented. The exported entities are properly defined and will be valuable for testing the model implementation.

src/plugins/devtools.ts (1)

24-24: Confirm the intention behind changing to XML representation

The modification from getValue() to toXML() changes the format of the editor model's content from plain text to XML in the Devtools plugin. This change aligns with the PR objective of applying a schema to the model, as XML is commonly used for structured data representation.

However, please consider the following points:

  1. Ensure that this change is intentional and consistent with the overall direction of the project.
  2. Verify that all consumers of the Devtools plugin output are updated to handle XML instead of plain text.
  3. Consider adding a comment explaining the rationale behind using XML representation for debugging purposes.

To ensure consistency across the codebase, let's check for other occurrences of getValue() that might need similar updates:

✅ Verification successful

To address the issue with the unrecognized file type, please run the following scripts to search for other occurrences of getValue() in TypeScript files:


Verified: No Additional Usages of getValue() Found

After searching the codebase, no other usages of getValue() were found beyond the method declaration in src/view/view.ts. This confirms that the change in src/plugins/devtools.ts is isolated and does not affect other parts of the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other occurrences of getValue() in the codebase
rg --type typescript 'getValue\(\)'

Length of output: 72


Script:

#!/bin/bash
# Search for getValue() in TypeScript files using the correct type identifier
rg --type ts 'getValue\(\)'

Length of output: 69


Script:

#!/bin/bash
# Alternative search for getValue() using a glob pattern for .ts files
rg 'getValue\(\)' -g '*.ts'

Length of output: 69

test/model/model.test.ts (2)

1-3: LGTM: Imports are appropriate and well-structured.

The import statements are concise and import only the necessary components. The use of a custom PlainTextSpec from a helper file is a good practice for test organization.


5-5: LGTM: Well-structured test suite.

The test suite is properly organized using a describe block for the 'model', which provides a clear context for the contained tests.

Also applies to: 23-23

src/model/nodepos.ts (2)

1-2: LGTM: Import statements are appropriate.

The import statements correctly bring in the necessary types and functions used in this file.


1-41: Overall assessment: Well-implemented utility functions with minor documentation improvements suggested.

The src/model/nodepos.ts file introduces three well-implemented utility functions for working with node positions and ranges. The logic in each function is correct and efficient. The only suggestion is to enhance the JSDoc comments for better documentation.

Great job on implementing these utility functions! They will likely prove very useful for working with node positions and ranges in the model.

test/model/schema.test.ts (1)

3-3: LGTM: New import for XML conversion.

The import of toXML function is correctly added and is necessary for the new test case.

src/model/types.ts (5)

1-4: LGTM: Node type definition is clear and flexible.

The Node type as a union of Text | Element provides a good foundation for representing different node types in the model. This approach allows for easy extension if new node types are needed in the future.


16-23: LGTM: Text type definition is clear and consistent.

The Text type definition is well-structured and consistent with the Element type. The fixed type property helps in easily identifying text nodes, and the optional parent property allows for flexibility in the node structure.


25-34: LGTM: Path type definition is simple and well-documented.

The Path type as an array of numbers is an elegant solution for representing positions in the editor content without direct node references. The detailed comment with examples greatly enhances the understanding of this concept.


53-65: LGTM: IndexRange type definition is well-documented and clear.

The IndexRange type effectively represents a range using start and end indexes in the XML-like content. The detailed comment with a visual example greatly enhances the understanding of this concept.


1-65: Overall, excellent type definitions for the editor model.

This file provides a comprehensive and well-structured set of types for the editor model. The use of descriptive comments, especially with examples, greatly enhances the understanding of each type. The types are designed to work together cohesively, providing a solid foundation for the model implementation.

A few minor suggestions were made to further improve the type definitions:

  1. Consider making the children property in Element non-optional with a default empty array.
  2. Expand the documentation for the Range type to include an example.
  3. Add a utility function to convert NodePos to Path.

These changes would further enhance the usability and clarity of the type system.

test/util/array.test.ts (2)

1-2: LGTM: Imports are correct and properly structured.

The import statements are appropriate for the test file, importing necessary testing functions from 'vitest' and the utility functions being tested.


1-52: Overall, excellent test coverage with minor suggestions for improvement.

This new test file provides comprehensive coverage for the groupBy and takeWhile utility functions. The tests are well-structured, clear, and cover various scenarios including edge cases. The suggestions for additional test cases (non-primitive types for groupBy and immediate termination for takeWhile) would further enhance the robustness of the test suite.

Great job on writing these tests! They will significantly contribute to the reliability of the utility functions.

src/utils/array.ts (3)

1-7: LGTM! Well-documented and type-safe implementation.

The firstOf function is correctly implemented, with proper type safety using generics. The JSDoc comment accurately describes the function's behavior, including the edge case of an empty array.


9-15: LGTM! Consistent with firstOf and well-documented.

The lastOf function is correctly implemented, maintaining consistency with the firstOf function in terms of type safety and documentation. The logic to access the last element is correct.


1-73: Overall, well-implemented array utility functions with good type safety.

This file provides a comprehensive set of array utility functions that are well-implemented and type-safe. The use of generics ensures flexibility and type correctness across different use cases. The suggested improvements are minor and focus on enhancing documentation and simplifying the groupBy function for better readability.

Great job on creating these utility functions! They will likely prove very useful throughout the project.

test/view/selection.test.ts (1)

2-2: LGTM: Import statement updated correctly.

The import statement has been properly updated to include the new pathOf function alongside offsetOf. This change aligns with the introduction of the new pathOf test suite.

src/view/view.ts (2)

1-1: LGTM: Import statements updated correctly.

The new imports align well with the changes made in the file, particularly for the handleBeforeInput method and the new getSelection method.

Also applies to: 4-5


Line range hint 1-82: Verify integration with the rest of the codebase.

The changes in this file look good and align well with the PR objectives. However, given that this is part of a larger refactoring effort to apply a schema to the model, it would be beneficial to verify how these changes integrate with other parts of the codebase.

To ensure smooth integration, please run the following script to check for any potential issues:

This script will help identify any inconsistencies or potential issues related to the recent changes in the View class.

test/model/nodes.test.ts (3)

1-12: LGTM: Import statements are appropriate and concise.

The import statements are well-organized and include all necessary testing utilities and functions required for the tests. There are no unused imports visible.


14-31: LGTM: 'Nodes.Pos' tests are comprehensive and well-structured.

The 'Nodes.Pos' describe block contains two well-written test cases that cover important functionality:

  1. Converting a Path to a Pos
  2. Converting a node to a path

Both tests use clear XML examples and appropriate assertions to verify the expected behavior.


1-100: Overall, excellent test coverage and implementation.

This new test file test/model/nodes.test.ts provides comprehensive coverage for node manipulation functionalities. The tests are well-structured, use clear examples, and cover important edge cases. The use of describe blocks helps organize the tests logically.

Key strengths:

  1. Clear and concise test cases
  2. Good coverage of different node manipulation scenarios
  3. Appropriate use of assertions and setup

Minor suggestions for improvement have been made to enhance code reusability and test isolation. These changes would further improve the maintainability and clarity of the tests.

Great job on implementing these tests! They will significantly contribute to the reliability of the node manipulation functions in the codebase.

src/commands/commands.ts (2)

5-7: Command type definition is appropriate

The Command type correctly defines a structure with an array of Operation objects, which will be used to represent a sequence of operations in a command.


9-19: insertText function correctly creates a text insert command

The insertText function properly constructs a command that inserts text at a given range. It encapsulates the operation details accurately, ensuring that text insertion is handled correctly.

src/editor.ts (4)

33-36: Default EditorOptions ensure a schema is always provided

The create method now defaults the opts parameter to { schema: BasicSchema }, which guarantees that a schema is supplied even if none is provided by the user. This change ensures that the editor has a schema to work with by default, and the optional initialValue and plugins are handled appropriately.


46-46: Model initialization updated to include schema

The Model.create method now takes opts.schema as the first argument. This ensures that the model is initialized with the correct schema, aligning the model with the provided or default schema specification.


58-58: Initialize the view with the model's XML representation

Updating this.view.setValue to use this.model.toXML() ensures that the view is initialized with the correct XML representation of the model's content. This change aligns the view's content with the model's structure.


135-140: insertText method correctly handles text insertion

The updated insertText method now retrieves the current selection from the view and defaults to the end of the model's content if no selection exists. This logic ensures that text insertion operates correctly in all scenarios. The use of this.execute(insertText(range, text)) effectively applies the insertion command to the model.

src/view/selection.ts (4)

1-2: Imports Are Correct

The import statements correctly include the necessary types and utility functions, ensuring that all dependencies are accounted for.


8-8: Position Type Definition

The Position type is appropriately defined as a tuple of a Node and a number, representing a position within the DOM. This aligns with its usage in the code and ensures type safety.


11-19: Implementation of getSelection Function

The getSelection function accurately retrieves the current selection from the window and converts it to a Range using the toRange function. The check for an empty selection is correctly handled, returning undefined when there is no selection.


37-73: pathOf Function Logic

The pathOf function effectively computes the path of a given position within the container by:

  • Traversing up the DOM tree to the container.
  • Grouping adjacent text nodes to treat them as a single node.
  • Calculating offsets within text node groups.

This approach ensures accurate path calculations within complex DOM structures.

src/model/schema.ts (2)

1-1: Imports correctly updated to use NoteNode and NoteElement

The imports are appropriately updated to reflect the new type definitions.


88-90: ⚠️ Potential issue

Rename inner function to prevent name shadowing

The fromDOM method contains an inner function also named fromDOM, which can cause confusion and potential scope issues. Rename the inner function to avoid shadowing the outer method.

Apply this diff to rename the inner function:

-    function fromDOM(domNode: Node): NoteNode {
+    function parseDOMNode(domNode: Node): NoteNode {

Ensure all references to the inner function are updated accordingly.

Likely invalid or redundant comment.

src/model/nodes.ts (2)

154-167: Check for negative index in insertAfter

Similarly, in the insertAfter function, verify that prev is always found in parent.children to prevent unexpected behavior when indexOf returns -1.

Run the following script to check the validity:

#!/bin/bash
# Description: Verify that 'prev' is always found in 'parent.children'

# Test: Search for all calls to 'insertAfter' and ensure 'prev' is a child of 'parent'
rg --type ts 'insertAfter\((\w+),' -A 5

136-149: Check for negative index in insertBefore

In the insertBefore function, the index of next in parent.children might be -1 if next is not found. Although this is unlikely if next is a valid child, adding a check can prevent unexpected behavior.

Run the following script to verify that next is always a child of parent:

✅ Verification successful

Verification Successful: No instances of insertBefore where next is not a child of parent were found.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that 'next' is always found in 'parent.children'

# Test: Search for all calls to 'insertBefore' and ensure 'next' is a child of 'parent'
rg --type ts 'insertBefore\((\w+),' -A 5

Length of output: 349


Script:

#!/bin/bash
# Description: Verify that 'next' is always found in 'parent.children'

# Test: Search for all calls to 'insertBefore' and ensure 'next' is a child of 'parent'
rg --type ts 'insertBefore\((\w+),' -A 5

Length of output: 42


Script:

#!/bin/bash
# Description: Verify that 'next' is always found in 'parent.children'

# Test 1: Refined rg command to search for all calls to 'insertBefore' with improved regex
rg --type ts 'insertBefore\s*\(\s*(\w+)\s*,' -A 5

# Test 2: Use ast-grep to find all 'insertBefore' function calls and extract the first argument
ast-grep --lang typescript --pattern 'call_expression:insertBefore($_, ...)' --json .

Length of output: 142

Comment on lines 5 to 8
export function isCollapsed(range: Range): boolean {
return range.s.length === range.e.length &&
range.s.every((s, i) => s === range.e[i]);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider handling edge cases.

While the current implementation works correctly for valid Range objects, it doesn't handle potential edge cases such as null or undefined values. Consider adding a null check at the beginning of the function.

Here's a suggested improvement:

export function isCollapsed(range: Range): boolean {
  if (!range || !range.s || !range.e) {
    return false; // or throw an error, depending on your error handling strategy
  }
  return range.s.length === range.e.length &&
    range.s.every((s, i) => s === range.e[i]);
}

test/model/model.test.ts Outdated Show resolved Hide resolved
src/commands/commands.ts Show resolved Hide resolved
src/editor.ts Outdated Show resolved Hide resolved
Comment on lines +60 to +65
const index =
takeWhile(groups[i], (c) => c !== node).reduce(
(acc, c) => acc + (c.textContent || '').length,
0,
) + offset;
path.unshift(index);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize Index Calculation in pathOf Function

The index calculation reduces over the text nodes to accumulate their lengths:

const index =
  takeWhile(groups[i], (c) => c !== node).reduce(
    (acc, c) => acc + (c.textContent || '').length,
    0,
  ) + offset;

Consider optimizing this calculation for better performance, especially when dealing with a large number of text nodes. Possible improvements include:

  • Caching text content lengths if the same calculations occur frequently.
  • Minimizing the use of high-order functions within performance-critical loops.

src/model/schema.ts Show resolved Hide resolved
src/model/schema.ts Outdated Show resolved Hide resolved
src/model/model.ts Show resolved Hide resolved
src/model/model.ts Show resolved Hide resolved
src/model/nodes.ts Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (13)
src/model/operations.ts (2)

9-17: LGTM: Well-defined EditOperation type, but consider updating comments.

The EditOperation type is well-structured for editing operations. However, there's a comment about a ReplaceOperation that isn't implemented.

Consider either removing the ReplaceOperation comment or updating it to clarify its future implementation status. For example:

/**
 * `EditOperation` represents an operation that edits a range of nodes.
 * 
 * Note: A future enhancement may include a `ReplaceOperation` to replace
 * a range of nodes with another range of nodes.
 */

1-27: LGTM: Well-structured type definitions aligning with PR objectives.

This file introduces clear and well-structured type definitions for editor operations, which aligns well with the PR objective of applying schema to the model. The Operation, EditOperation, and MoveOperation types provide a solid foundation for implementing and managing editor operations.

The exported types will be useful for other parts of the codebase that need to work with these operations. Overall, this is a good addition to the project's type system.

As the project evolves, consider the following:

  1. Implement the mentioned ReplaceOperation if it becomes necessary.
  2. Ensure that these types are used consistently across the codebase when dealing with editor operations.
  3. Consider adding utility functions or methods to work with these types, such as type guards or operation creators, in a separate file to keep this file focused on type definitions.
src/editor.ts (3)

8-8: LGTM: Schema support added

The import of SchemaSpec and BasicSchema indicates the addition of schema support to the editor. This is a valuable feature for content structure and validation.

Consider adding documentation comments to explain the purpose and usage of these schema-related imports, especially for developers who might be new to the concept.


15-15: LGTM: Schema option added with good default

The addition of the schema property to EditorOptions and the use of BasicSchema as a default in the create method is a good approach. It allows for custom schemas while maintaining backwards compatibility.

Consider adding a brief comment above the schema property in EditorOptions to explain its purpose and any requirements for custom schemas.

Also applies to: 33-36


135-140: LGTM: Improved insertText method

The refactored insertText method now correctly handles the case when there's no current selection by using the model's content end range. This is a good improvement in the text insertion logic.

Consider adding a brief comment explaining the fallback behavior when there's no selection, to make the code more self-documenting.

src/view/selection.ts (2)

21-32: LGTM: toRange function is well-implemented, with a minor suggestion.

The toRange function correctly converts an AbstractRange to a Range, handling both collapsed and non-collapsed ranges appropriately. It makes good use of the pathOf function.

Consider adding a type annotation for the return value to improve code readability:

export function toRange(range: AbstractRange, container: Node): Range {
  // ... existing implementation ...
}

34-73: LGTM: pathOf function is well-implemented, with suggestions for improvement.

The pathOf function correctly computes the path of a node within a container, handling the complexities of DOM traversal and text node grouping.

Consider the following improvements:

  1. Extract the text node handling logic into a separate function to improve readability:
function calculateTextNodeIndex(group: Node[], node: Node, offset: number): number {
  return takeWhile(group, (c) => c !== node).reduce(
    (acc, c) => acc + (c.textContent || '').length,
    0
  ) + offset;
}
  1. Use early returns to reduce nesting:
if (node.nodeType === Node.TEXT_NODE) {
  const index = calculateTextNodeIndex(groups[i], node as Node, offset);
  path.unshift(index);
}

path.unshift(i);
node = parent;

These changes would make the function more modular and easier to understand.

src/model/schema.ts (2)

11-18: LGTM: Useful addition of BasicSchema

The introduction of BasicSchema is a valuable addition, providing a default schema for a simple text editor. This serves as a good example for users and can be a helpful starting point.

Consider adding a brief comment for each node type in the schema to explain their purpose, e.g.:

export const BasicSchema: SchemaSpec = {
  root: { children: 'p*' }, // Root node, can contain multiple paragraphs
  p: { children: 'text*' }, // Paragraph node, can contain multiple text nodes
  text: {}, // Text node, represents plain text content
};

This would further enhance the documentation and make the schema even more self-explanatory.


88-136: LGTM: Valuable addition of fromDOM and fromXML methods

The new fromDOM and fromXML methods are excellent additions to the Schema class, significantly enhancing its capabilities for working with different data formats. The implementation is thorough and handles various scenarios well.

A few suggestions for further improvement:

  1. Consider adding input validation for the fromXML method to ensure the input is a non-empty string.
  2. In the fromDOM method, you might want to handle potential errors from DOMParser when parsing invalid XML.
  3. For better type safety, consider using a type guard for the domElem as Element cast in the fromDOM method.

Example for point 3:

if (domNode instanceof Element) {
  const attrs: Record<string, string> = {};
  for (const { name, value } of domNode.attributes) {
    attrs[name] = value;
  }
  if (Object.keys(attrs).length > 0) {
    node.attrs = attrs;
  }
}

These changes would make the methods more robust and type-safe.

src/model/nodes.ts (4)

11-24: LGTM: toXML function with suggestion for enhancement

The toXML function correctly converts nodes to their XML string representation, handling both text and element nodes appropriately. The recursive approach for element nodes is well-implemented.

Consider enhancing the function to handle element attributes if they are part of your Element type. This would provide a more complete XML representation.


26-37: LGTM: toNodePos function with suggestion for error handling

The toNodePos function effectively converts a path to a node position. The use of initialOf and lastOf utility functions is appropriate.

Consider adding error handling for cases where the path is invalid or intermediate nodes are missing. This would make the function more robust and prevent potential runtime errors.


112-132: LGTM: splitText function with minor optimization suggestion

The splitText function correctly splits a text node at a given position, handling edge cases appropriately. The implementation is correct and efficient.

Consider a minor optimization: you can combine the creation and insertion of the new node into a single step:

return insertAfter(text, { type: 'text', text: next })[0] as Text;

This eliminates the need for the intermediate newNode variable.


152-168: LGTM: insertAfter function with refactoring suggestion

The insertAfter function correctly inserts new nodes after a given node. It properly handles multiple nodes and updates parent references.

Consider refactoring insertBefore and insertAfter to reduce code duplication. You could create a private helper function that both methods use:

function insertNodes(sibling: Node, newNodes: Array<Node>, after: boolean): Array<Node> {
  const parent = sibling.parent;
  if (!parent) {
    throw new Error('node does not have a parent');
  }

  const index = parent.children!.indexOf(sibling) + (after ? 1 : 0);
  parent.children!.splice(index, 0, ...newNodes);
  for (const node of newNodes) {
    node.parent = parent;
  }

  return newNodes;
}

export function insertBefore(next: Node, ...newNodes: Array<Node>): Array<Node> {
  return insertNodes(next, newNodes, false);
}

export function insertAfter(prev: Node, ...newNodes: Array<Node>): Array<Node> {
  return insertNodes(prev, newNodes, true);
}

This approach would reduce code duplication and make future maintenance easier.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 2813043 and 4b000cf.

📒 Files selected for processing (23)
  • src/commands.ts (0 hunks)
  • src/commands/commands.ts (1 hunks)
  • src/editor.ts (6 hunks)
  • src/main.ts (1 hunks)
  • src/model/model.ts (1 hunks)
  • src/model/node.ts (0 hunks)
  • src/model/nodepos.ts (1 hunks)
  • src/model/nodes.ts (1 hunks)
  • src/model/operations.ts (1 hunks)
  • src/model/range.ts (1 hunks)
  • src/model/schema.ts (4 hunks)
  • src/model/types.ts (1 hunks)
  • src/plugins/devtools.ts (1 hunks)
  • src/range.ts (0 hunks)
  • src/utils/array.ts (1 hunks)
  • src/view/selection.ts (2 hunks)
  • src/view/view.ts (3 hunks)
  • test/model/helper.ts (1 hunks)
  • test/model/model.test.ts (1 hunks)
  • test/model/nodes.test.ts (1 hunks)
  • test/model/schema.test.ts (2 hunks)
  • test/util/array.test.ts (1 hunks)
  • test/view/selection.test.ts (3 hunks)
💤 Files with no reviewable changes (3)
  • src/commands.ts
  • src/model/node.ts
  • src/range.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/commands/commands.ts
  • src/main.ts
  • src/model/range.ts
  • src/model/types.ts
  • src/plugins/devtools.ts
  • src/utils/array.ts
  • src/view/view.ts
  • test/model/helper.ts
  • test/model/model.test.ts
  • test/model/nodes.test.ts
  • test/model/schema.test.ts
  • test/util/array.test.ts
  • test/view/selection.test.ts
🧰 Additional context used
🔇 Additional comments (36)
src/model/operations.ts (3)

1-1: LGTM: Imports are appropriate.

The imports of Node and Range from './types' are suitable for the operations being defined in this file.


3-7: LGTM: Well-defined Operation type.

The Operation type is well-defined as a union of EditOperation and MoveOperation. The comment provides clear documentation, and the naming follows TypeScript conventions.


19-27: LGTM: Well-defined MoveOperation type.

The MoveOperation type is well-structured for move operations. The comment provides clear documentation, and the use of Range for both source and target properties is appropriate for specifying the original and new positions of the nodes.

src/model/nodepos.ts (5)

1-2: LGTM: Import statements are appropriate.

The import statements are concise and import the necessary types and functions for this module.


4-9: LGTM: isLeftMost function is well-implemented and documented.

The function correctly checks if a node position is at the leftmost position by verifying if the offset is 0. The JSDoc comment accurately describes the function's purpose.


11-16: LGTM: isRightMost function is well-implemented and documented.

The function correctly checks if a node position is at the rightmost position by comparing the offset to the node's length (obtained using the lengthOf function). The JSDoc comment accurately describes the function's purpose.


18-23: LGTM: equals function is well-implemented and documented.

The function correctly checks if two node positions are equal by comparing both their node and offset properties. The JSDoc comment accurately describes the function's purpose.


1-23: Excellent implementation of node position utility functions.

This file provides a set of well-implemented and clearly documented utility functions for working with node positions. The code is clean, concise, and follows good practices. Each function serves a specific purpose and is appropriately exported for use in other modules.

src/editor.ts (4)

7-7: LGTM: Good separation of concerns

The import of Command, execute, and insertText from a separate commands module improves the code's modularity and organization.


46-46: LGTM: Model creation updated to include schema

The Model.create call now includes the opts.schema parameter, ensuring that the model is created with the specified or default schema. This is a good integration of the new schema functionality.


58-58: LGTM: Updated model content representation

The change from getValue() to toXML() suggests an improvement in how the model's content is represented. This likely provides better structure or formatting for the view.


71-78: Implement the model-to-view update in the model's subscribe callback

The subscription to the model changes currently contains a TODO to update the view, but the implementation is missing. To ensure the view reflects changes in the model, you should update the view within this callback.

Would you like assistance in implementing the update logic for the view? I can help provide code to synchronize the view with the model when the model changes.

src/view/selection.ts (3)

1-2: LGTM: Imports and type definition look good.

The new imports and the Position type definition enhance the module's functionality and improve code readability. The Position type, representing a DOM node and an offset, is well-documented and will be useful for the new functions.

Also applies to: 4-8


11-19: LGTM: getSelection function is well-implemented.

The getSelection function correctly retrieves the current selection and handles the case when no selection is present. It appropriately uses the toRange function to convert the selection to a Range.


60-65: Address previous optimization suggestion for pathOf function.

A previous review comment suggested optimizing the index calculation in the pathOf function. This optimization has not been implemented in the current changes.

Consider revisiting this optimization suggestion:

  1. Cache text content lengths if the same calculations occur frequently.
  2. Minimize the use of high-order functions within performance-critical loops.

These optimizations could improve performance, especially when dealing with a large number of text nodes.

src/model/schema.ts (5)

1-1: LGTM: Improved import naming

The change to import Node as NoteNode and Element as NoteElement improves clarity and helps avoid potential naming conflicts. This is a good practice, especially in larger codebases.


3-6: LGTM: Improved type definition and documentation

The explicit export of SchemaSpec and the added documentation comment enhance the code's usability and maintainability. This change promotes better type checking across modules and improves developer understanding of the type's purpose.


9-9: LGTM: Improved type safety for TextNodeSpec

The change from {} to Record<string, never> for TextNodeSpec improves type safety and follows best practices. This addresses the concern raised in the previous review comment about avoiding {} as a type. Good job on implementing this improvement.


22-22: LGTM: Improved class documentation

The updated documentation for the Schema class now accurately reflects its expanded capabilities, including building models from DOM, JSON, or XML. This change provides users with a clear understanding of the class's functionality and is a valuable improvement to the codebase.


32-42: Extend support for non-text nodes in create method

The create method now includes basic functionality for creating text nodes and validates the node type. This is a good start, but as mentioned in the previous review, consider extending this method to support additional node types defined in the schema.

For non-text nodes, you could create an element node with an empty children array:

if (type !== 'text') {
  return { type, attrs: {}, children: [] };
}

This would provide a more complete implementation and avoid throwing a 'not implemented' error.

src/model/model.ts (10)

2-19: LGTM: Import statements and class declaration updates

The new imports and the change in the Model class declaration accurately reflect the transition to an operation-based model. These changes align well with the overall restructuring of the Model class.


23-27: LGTM: New static create method

The create static method is a well-designed addition. It encapsulates the logic for creating a new Model instance, including schema creation and initial XML parsing. This approach promotes cleaner instantiation of the Model class.


29-37: LGTM: Updated constructor and new createText method

The constructor changes appropriately reflect the new model structure. The addition of the createText method provides a consistent and schema-aware way to create text nodes, which is a good practice for maintaining data integrity.


39-41: LGTM: toXML method implementation

The toXML method provides a clean way to convert the root node back to XML format. This is essential for serialization and maintaining compatibility with XML-based systems.


43-52: Refine error message in apply method

The error message in the default case of the switch statement includes the entire op object, which could potentially expose sensitive information or clutter logs.

As previously suggested, consider modifying the error message to include only the operation type:

- throw new Error(`invalid operation type: ${op}`);
+ throw new Error(`invalid operation type: ${op.type}`);

54-90: LGTM: Well-implemented edit method

The edit method is well-structured and handles node removal and insertion effectively. It correctly manages edge cases like collapsed ranges and uses appropriate utility functions for node manipulation. The return of an operation object aligns well with the new operation-based model structure.


95-110: LGTM: Well-implemented nodePosOf method

The nodePosOf method effectively retrieves node positions from paths and includes a useful text node splitting functionality. This implementation provides precise positioning capabilities, which is crucial for accurate text manipulation operations.


116-137: LGTM: Well-implemented getContentEndRange method

The getContentEndRange method effectively determines the end range of content within the model. It correctly handles both text and element nodes, and the traversal logic is clear and well-implemented. This method will be useful for operations that need to target the end of the document.


140-141: Implement the 'move' method

The move method is still not implemented, which will cause runtime errors when applying 'move' operations. This issue was previously flagged and remains unaddressed.

As mentioned in the previous review, please implement the move method to handle 'move' operations correctly. If you need assistance with the implementation, please let me know.


1-142: Overall assessment: Significant improvements with one remaining task

The changes to the Model class represent a substantial improvement in structure and functionality. The transition to an operation-based model is well-implemented, with clear and effective methods for handling various operations. The new structure should provide better flexibility and maintainability.

However, there is one remaining task:

  1. Implement the move method to handle 'move' operations.

Once this is addressed, the Model class will be fully functional and aligned with the new operation-based approach.

src/model/nodes.ts (6)

1-9: LGTM: Imports and TagType definition

The imports are appropriate for the file's functionality, and the TagType definition is clear and well-documented.


39-56: LGTM: pathOf function

The pathOf function effectively computes the path of a node from the container. The implementation is correct and efficient, using a while loop to traverse up the tree and unshift to build the path. The error handling for cases where the node is not in the container is appropriate.


103-110: LGTM: lengthOf function

The lengthOf function correctly calculates the length of a node, handling both text and element nodes appropriately. For text nodes, it returns the length of the text, and for element nodes, it returns the number of children. The implementation is concise and effective.


134-150: LGTM: insertBefore function

The insertBefore function correctly inserts new nodes before a given node. It efficiently handles multiple nodes using the spread operator and properly updates the parent references. The error handling for the case where the node has no parent is appropriate.


170-186: LGTM: removeNode function

The removeNode function correctly removes a node from its parent. It includes appropriate error handling for cases where the node has no parent or is not in the parent's children array. The use of splice for removal is efficient and correct.


1-186: Overall assessment: Well-implemented node management utilities with room for minor improvements

The src/model/nodes.ts file provides a comprehensive set of utility functions for managing a tree structure of nodes. The implementation is generally solid, with clear and efficient functions for XML conversion, path management, and node manipulation.

Key points:

  1. Most functions are well-implemented and appropriately handle error cases.
  2. The nodesBetween function requires fixing to address the inRange variable issue and improve handling of same-node cases.
  3. There are opportunities for minor optimizations in splitText and refactoring to reduce code duplication between insertBefore and insertAfter.

Addressing these points will further improve the robustness and maintainability of the code.

To ensure that the changes suggested for the nodesBetween function don't introduce regressions, please run the following verification script:

This script will help verify the implementation of the nodesBetween function and its usage across the codebase, as well as identify any existing test cases that may need to be updated after applying the suggested changes.

✅ Verification successful

Verification Complete: nodesBetween Function is Implemented and Tested

The nodesBetween function is present in src/model/nodes.ts and is utilized within test/model/nodes.test.ts, indicating that it is implemented and has associated test coverage.

  • Implementation Found:

    • src/model/nodes.ts contains the nodesBetween function definition.
  • Usage in Tests:

    • test/model/nodes.test.ts includes usages of nodesBetween, suggesting existing tests cover its functionality.

To ensure comprehensive coverage, please review the existing tests in test/model/nodes.test.ts to verify that all edge cases, particularly regarding the inRange variable and same-node scenarios, are adequately tested.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the nodesBetween function implementation and its usage

# Test: Search for nodesBetween function implementation
echo "Searching for nodesBetween function implementation:"
rg --type typescript -A 5 'function\*?\s+nodesBetween'

# Test: Search for nodesBetween function usage
echo "Searching for nodesBetween function usage:"
rg --type typescript 'nodesBetween\('

# Test: Search for test cases related to nodesBetween
echo "Searching for test cases related to nodesBetween:"
rg --type typescript 'test.*nodesBetween'

Length of output: 562


Script:

#!/bin/bash
# Description: Verify the nodesBetween function implementation and its usage

# Test: Search for nodesBetween function implementation
echo "Searching for nodesBetween function implementation:"
rg 'function\*?\s+nodesBetween' --glob '*.ts' --glob '*.tsx' -A 5

# Test: Search for nodesBetween function usage
echo "Searching for nodesBetween function usage:"
rg 'nodesBetween\(' --glob '*.ts' --glob '*.tsx'

# Test: Search for test cases related to nodesBetween
echo "Searching for test cases related to nodesBetween:"
rg 'test.*nodesBetween' --glob '*.ts' --glob '*.tsx'

Length of output: 1218

Comment on lines 58 to 101
/**
* `nodesBetween` iterates over the nodes between the start and end positions.
*/
export function* nodesBetween(
container: Element,
start: NodePos,
end: NodePos,
): Generator<[Node, TagType]> {
let inRange = false;
function* traverse(node: Node): Generator<[Node, TagType]> {
if (node === start.node) {
inRange = true;
}

if (node.type === 'text') {
if (inRange) {
yield [node, 'text'];
}
} else {
const elem = node as Element;
for (const child of elem.children || []) {
if (child.type !== 'text' && inRange) {
yield [child, 'open'];
}

yield* traverse(child);

if (child.type !== 'text' && inRange) {
yield [child, 'close'];
}
}
}

if (node === end.node) {
inRange = false;
}
}

if (equals(start, end)) {
return;
}

yield* traverse(container);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the incorrect use of the inRange variable and improve same node handling

The nodesBetween function has two issues that need to be addressed:

  1. The inRange variable is still being used incorrectly, as mentioned in the past review comment. This can lead to incorrect behavior due to shared state across recursion levels.

  2. The function doesn't handle the case where start and end nodes are the same but with different offsets.

To fix these issues, apply the following changes:

  1. Pass inRange as a parameter to the traverse function:
- let inRange = false;
- function* traverse(node: Node): Generator<[Node, TagType]> {
+ function* traverse(node: Node, inRange: boolean): Generator<[Node, TagType]> {
    if (node === start.node) {
-     inRange = true;
+     inRange = true;
    }

    // ... rest of the function ...

    if (node === end.node) {
-     inRange = false;
+     inRange = false;
    }
  }

- yield* traverse(container);
+ yield* traverse(container, false);
  1. Update the equality check to handle same node with different offsets:
- if (equals(start, end)) {
+ if (start.node === end.node && start.offset === end.offset) {
    return;
  }

These changes will ensure correct traversal and handling of all cases, including when start and end positions are in the same node.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* `nodesBetween` iterates over the nodes between the start and end positions.
*/
export function* nodesBetween(
container: Element,
start: NodePos,
end: NodePos,
): Generator<[Node, TagType]> {
let inRange = false;
function* traverse(node: Node): Generator<[Node, TagType]> {
if (node === start.node) {
inRange = true;
}
if (node.type === 'text') {
if (inRange) {
yield [node, 'text'];
}
} else {
const elem = node as Element;
for (const child of elem.children || []) {
if (child.type !== 'text' && inRange) {
yield [child, 'open'];
}
yield* traverse(child);
if (child.type !== 'text' && inRange) {
yield [child, 'close'];
}
}
}
if (node === end.node) {
inRange = false;
}
}
if (equals(start, end)) {
return;
}
yield* traverse(container);
}
/**
* `nodesBetween` iterates over the nodes between the start and end positions.
*/
export function* nodesBetween(
container: Element,
start: NodePos,
end: NodePos,
): Generator<[Node, TagType]> {
function* traverse(node: Node, inRange: boolean): Generator<[Node, TagType]> {
if (node === start.node) {
inRange = true;
}
if (node.type === 'text') {
if (inRange) {
yield [node, 'text'];
}
} else {
const elem = node as Element;
for (const child of elem.children || []) {
if (child.type !== 'text' && inRange) {
yield [child, 'open'];
}
yield* traverse(child, inRange);
if (child.type !== 'text' && inRange) {
yield [child, 'close'];
}
}
}
if (node === end.node) {
inRange = false;
}
}
if (start.node === end.node && start.offset === end.offset) {
return;
}
yield* traverse(container, false);
}

@hackerwins hackerwins changed the title [Draft] Apply schema to model Apply schema to model Oct 19, 2024
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (19)
src/utils/array.ts (3)

17-22: LGTM with a minor suggestion: Well-implemented initialOf function

The initialOf function is correctly implemented, with an appropriate type signature and accurate JSDoc comment. It efficiently returns all elements of the array except the last one.

Consider handling the case of an empty array explicitly to improve robustness:

export function initialOf<T>(array: Array<T>): Array<T> {
  return array.length > 0 ? array.slice(0, -1) : [];
}

This change also simplifies the slice operation by using a negative index.


31-55: LGTM with suggestions: Well-implemented groupBy function

The groupBy function is correctly implemented, with an appropriate type signature and accurate JSDoc comment. It efficiently groups adjacent elements of an array according to the given function.

Consider the following suggestions to improve readability:

  1. Use destructuring to simplify access to the last group and its last element.
  2. Use push instead of direct index assignment for clarity.
  3. Consider using if-else instead of ternary operator for the initial group creation.

Here's a refactored version:

export function groupBy<T>(
  array: Array<T>,
  fn: (prev: T, curr: T) => boolean,
): Array<Array<T>> {
  if (array.length === 0) {
    return [];
  }

  return array.slice(1).reduce(
    (groups: Array<Array<T>>, current: T): Array<Array<T>> => {
      const [lastGroup] = groups.slice(-1);
      const [lastElement] = lastGroup.slice(-1);

      if (fn(lastElement, current)) {
        lastGroup.push(current);
      } else {
        groups.push([current]);
      }

      return groups;
    },
    [[array[0]]],
  );
}

This refactored version maintains the same functionality while improving readability.


57-73: LGTM with a minor suggestion: Well-implemented takeWhile function

The takeWhile function is correctly implemented, with an appropriate type signature and efficient logic. It accurately takes elements from the beginning of an array while the given predicate function returns true.

The JSDoc comment has a minor grammatical issue. Consider updating it to:

/**
 * `takeWhile` takes elements from the beginning of an array while the given
 * predicate function returns true.
 */

This small change improves the clarity of the comment.

src/model/nodepos.ts (3)

8-16: LGTM: toNodePos function is well-implemented.

The function correctly converts a path to a node position by traversing the container's children. It handles the last offset appropriately in the returned NodePos.

Consider adding error handling for cases where the path might be invalid or a node in the path doesn't exist. This could prevent potential runtime errors.


21-61: LGTM: nodesBetween function is well-implemented with efficient traversal.

The generator function correctly iterates over nodes between start and end positions, handling both text nodes and elements appropriately. The use of a generator is efficient for potentially large structures.

Consider extracting the traverse function to improve readability. This would make the main function body more concise and easier to understand at a glance. Here's a suggested refactor:

function* traverse(node: Node, start: NodePos, end: NodePos): Generator<[Node, TagType]> {
  // ... existing traverse logic ...
}

export function* nodesBetween(
  container: Element,
  start: NodePos,
  end: NodePos,
): Generator<[Node, TagType]> {
  if (equals(start, end)) {
    return;
  }

  yield* traverse(container, start, end);
}

73-75: LGTM: isRightMost function is correctly implemented.

The function accurately determines if a node position is at the rightmost position by comparing the offset to the length of the node using the lengthOf function.

Consider caching the result of lengthOf(pos.node) if it's an expensive operation and this function is called frequently. This could potentially improve performance in scenarios where the node length doesn't change often.

export function isRightMost(pos: NodePos): boolean {
  const nodeLength = lengthOf(pos.node);
  return pos.offset === nodeLength;
}
test/view/selection.test.ts (3)

13-13: Good use of template literal for HTML content.

The change to use a template literal with the html comment improves readability and enables better syntax highlighting in IDEs. This is a good practice.

Consider applying this change consistently to all HTML strings in the file for uniformity.


29-58: Well-structured test suite for pathOf function.

The new test suite for pathOf is comprehensive and mirrors the structure of the offsetOf suite, which is excellent for consistency. It covers various scenarios including simple conversion, nested elements, and error handling.

For improved readability, consider adding a brief comment before each test case explaining what aspect of pathOf it's testing. For example:

// Test conversion of root-level text node
it('should convert to the given position', () => {
  // ...
});

// Test conversion in nested elements
it('should convert to the given position in element', () => {
  // ...
});

// Test error handling for invalid input
it('should return error if the range is not in the container', () => {
  // ...
});

Line range hint 4-58: Consider refactoring to reduce duplication between test suites.

The offsetOf and pathOf test suites have very similar structures and use identical HTML content for testing. While this consistency is good, there's an opportunity to reduce duplication and improve maintainability.

Consider refactoring the common setup code into a shared function or using beforeEach. For example:

describe('Selection functions', () => {
  let container: HTMLDivElement;

  beforeEach(() => {
    container = document.createElement('div');
    container.innerHTML = /*html*/ `<b>Hello</b> <i>World</i>!`;
  });

  describe('offsetOf', () => {
    // offsetOf tests using shared container
  });

  describe('pathOf', () => {
    // pathOf tests using shared container
  });
});

This approach would make it easier to maintain and update both test suites simultaneously if the HTML structure needs to change in the future.

src/view/view.ts (1)

55-58: Consider creating issues for TODO items.

There are several TODO comments in the handleBeforeInput method that highlight areas needing further attention:

  1. Aggregating consecutive input events for composition text.
  2. Handling more input types created by the enter key.
  3. Handling the dummy br element created by the enter key.

Consider creating separate GitHub issues for each of these items to ensure they are tracked and addressed in future updates.

Would you like me to create GitHub issues for these TODO items?

src/model/nodes.ts (4)

12-22: LGTM with a suggestion: Consider handling element attributes in toXML function.

The toXML function correctly handles both text and element nodes, using recursion for element children. However, it doesn't currently process element attributes.

Consider extending the function to handle element attributes if they are part of your Element type. This could be implemented as follows:

export function toXML(node: Node): string {
  if (node.type === 'text') {
    return (node as Text).text;
  }

  const elem = node as Element;
  const attributes = elem.attributes 
    ? Object.entries(elem.attributes)
        .map(([key, value]) => ` ${key}="${value}"`)
        .join('')
    : '';
  const children = elem.children?.map(toXML).join('') || '';
  
  return `<${elem.type}${attributes}>${children}</${elem.type}>`;
}

This assumes that Element has an optional attributes property of type Record<string, string>.


55-72: LGTM with a suggestion: Consider returning both nodes in splitText function.

The splitText function correctly splits a text node at the given position and handles edge cases well. However, it modifies the original node in place, which might be unexpected.

Consider returning both the original (modified) node and the new node. This makes the function's behavior more explicit and allows the caller to decide how to use both resulting nodes. Here's a suggested modification:

export function splitText(pos: NodePos): [Text, Text] | undefined {
  if (pos.node.type !== 'text') {
    return undefined;
  }

  const text = pos.node as Text;

  if (pos.offset === 0 || pos.offset === text.text.length) {
    return undefined;
  }

  const prevText = text.text.slice(0, pos.offset);
  const nextText = text.text.slice(pos.offset);

  text.text = prevText;
  const newNode = { type: 'text', text: nextText } as Text;
  insertAfter(text, newNode);

  return [text, newNode];
}

This approach makes it clear that the original node is modified and a new node is created.


98-111: LGTM with a suggestion: Consider refactoring insertBefore and insertAfter.

The insertAfter function correctly inserts new nodes after a given node and is well-implemented. However, it's very similar to insertBefore.

Consider refactoring insertBefore and insertAfter to reduce code duplication. You could create a private helper function that both can use:

function insertNodes(
  referenceNode: Node,
  newNodes: Array<Node>,
  insertBefore: boolean
): Array<Node> {
  const parent = referenceNode.parent;
  if (!parent) {
    throw new Error('node does not have a parent');
  }

  const index = parent.children!.indexOf(referenceNode);
  const insertionIndex = insertBefore ? index : index + 1;
  parent.children!.splice(insertionIndex, 0, ...newNodes);
  for (const node of newNodes) {
    node.parent = parent;
  }

  return newNodes;
}

export function insertBefore(next: Node, ...newNodes: Array<Node>): Array<Node> {
  return insertNodes(next, newNodes, true);
}

export function insertAfter(prev: Node, ...newNodes: Array<Node>): Array<Node> {
  return insertNodes(prev, newNodes, false);
}

This approach reduces code duplication and makes it easier to maintain both functions.


116-129: LGTM with a suggestion: Update removed node's parent reference in removeNode.

The removeNode function correctly removes a node from its parent's children array and includes appropriate error handling.

Consider updating the removed node's parent reference to null to avoid potential issues with dangling references. Here's a suggested modification:

export function removeNode(node: Node) {
  const parent = node.parent;
  if (!parent) {
    throw new Error('node does not have a parent');
  }

  const children = parent.children!;
  const index = children.indexOf(node);
  if (index === -1) {
    throw new Error('node is not in the parent');
  }

  children.splice(index, 1);
  node.parent = null;  // Update the removed node's parent reference
}

This ensures that the removed node no longer references its former parent, which can help prevent bugs and make garbage collection more efficient.

src/view/selection.ts (2)

11-19: LGTM: getSelection function is well-implemented.

The getSelection function correctly retrieves the current selection and converts it to a Range. It properly handles the case when no selection is present.

Consider adding a type annotation for the selection variable to improve code readability:

const selection: Selection | null = window.getSelection();

21-32: LGTM: toRange function is well-implemented.

The toRange function correctly converts an AbstractRange to a Range, handling both collapsed and non-collapsed ranges appropriately.

For improved clarity, consider adding a comment explaining the difference between AbstractRange and Range:

/**
 * `toRange` converts the abstract range to the path range.
 * AbstractRange is a browser-provided type, while Range is our custom type using paths.
 */
src/model/model.ts (3)

22-32: LGTM: Static create method and constructor updated appropriately.

The new create method and the updated constructor align well with the new structure of the Model class. However, consider adding a comment to explain the purpose of the create method, especially its relationship to the constructor.

Consider adding a comment above the create method:

/**
 * Creates a new Model instance from a schema specification and initial XML.
 * This method handles the creation of the Schema and parsing of the initial value.
 */
static create(spec: SchemaSpec, initialValue: string): Model {
  // ... existing code ...
}

58-91: LGTM: edit method implemented correctly with a suggestion for improvement.

The edit method is well-structured and handles various scenarios correctly. The comment explaining the need to split the text node at the end of the range is helpful.

Consider adding type annotations to the for...of loop for better readability:

for (const [node, type] of nodesBetween(this.root, start, end) as [Node, string][]) {
  // ... existing code ...
}

This makes it clear what types node and type are expected to be.


96-107: Current implementation of rangeOf is functional but needs improvement.

The rangeOf method works for the current implementation, but as noted in the TODO comment, it assumes the first and last nodes are always text nodes. This limitation could lead to issues in more complex scenarios.

To ensure this limitation is addressed in the future:

  1. Would you like me to create a GitHub issue to track the implementation of handling element nodes in the rangeOf method?

  2. Consider adding a more detailed comment explaining the current limitations and the expected behavior for element nodes. This will help future developers understand the context of the TODO.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 4b000cf and 93dd420.

📒 Files selected for processing (24)
  • src/commands.ts (0 hunks)
  • src/commands/commands.ts (1 hunks)
  • src/editor.ts (6 hunks)
  • src/main.ts (1 hunks)
  • src/model/model.ts (1 hunks)
  • src/model/node.ts (0 hunks)
  • src/model/nodepos.ts (1 hunks)
  • src/model/nodes.ts (1 hunks)
  • src/model/operations.ts (1 hunks)
  • src/model/range.ts (1 hunks)
  • src/model/schema.ts (4 hunks)
  • src/model/types.ts (1 hunks)
  • src/plugins/devtools.ts (1 hunks)
  • src/range.ts (0 hunks)
  • src/utils/array.ts (1 hunks)
  • src/view/selection.ts (2 hunks)
  • src/view/view.ts (3 hunks)
  • test/model/helper.ts (1 hunks)
  • test/model/model.test.ts (1 hunks)
  • test/model/nodepos.test.ts (1 hunks)
  • test/model/nodes.test.ts (1 hunks)
  • test/model/schema.test.ts (2 hunks)
  • test/util/array.test.ts (1 hunks)
  • test/view/selection.test.ts (3 hunks)
💤 Files with no reviewable changes (3)
  • src/commands.ts
  • src/model/node.ts
  • src/range.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/commands/commands.ts
  • src/editor.ts
  • src/main.ts
  • src/model/operations.ts
  • src/model/range.ts
  • src/model/schema.ts
  • src/model/types.ts
  • src/plugins/devtools.ts
  • test/model/helper.ts
  • test/model/model.test.ts
  • test/model/nodes.test.ts
  • test/model/schema.test.ts
  • test/util/array.test.ts
🧰 Additional context used
🔇 Additional comments (32)
src/utils/array.ts (4)

1-7: LGTM: Well-implemented firstOf function

The firstOf function is correctly implemented, with an appropriate type signature and accurate JSDoc comment. It efficiently returns the first element of the array or undefined for empty arrays.


9-15: LGTM: Well-implemented lastOf function

The lastOf function is correctly implemented, with an appropriate type signature and accurate JSDoc comment. It efficiently returns the last element of the array or undefined for empty arrays.


24-29: LGTM: Well-implemented tailOf function

The tailOf function is correctly implemented, with an appropriate type signature and accurate JSDoc comment. It efficiently returns all elements of the array except the first one, and handles empty arrays correctly.


1-73: Overall assessment: Well-implemented array utility functions

This file introduces a set of useful array utility functions that are generally well-implemented. The functions are concise, efficient, and come with appropriate type signatures and JSDoc comments. Minor suggestions have been made for some functions to improve readability or robustness, but these are not critical issues. The utility functions provided here will likely be valuable for array manipulations throughout the project.

src/model/nodepos.ts (4)

1-3: LGTM: Import statements are appropriate.

The import statements are concise and relevant to the functionality implemented in this file. They import necessary types, utility functions, and constants from various modules.


66-68: LGTM: isLeftMost function is correctly implemented.

The function provides a simple and accurate check for determining if a node position is at the leftmost position by verifying if the offset is 0.


80-82: LGTM: equals function is correctly implemented.

The function provides a simple and accurate comparison of two node positions by checking both the node and offset equality. This implementation ensures that two positions are considered equal only if they refer to the same node and the same offset within that node.


1-82: Overall, the implementation in src/model/nodepos.ts is well-structured and robust.

The file introduces several utility functions for working with node positions, which appear to be crucial for the schema application to the model. The functions are generally well-implemented, with clear purposes and efficient algorithms.

A few minor suggestions were made for potential improvements:

  1. Consider adding error handling in toNodePos for invalid paths.
  2. The nodesBetween function could be refactored for improved readability.
  3. Performance optimization in isRightMost might be beneficial if it's frequently called.

These suggestions are minor and the current implementation is already of high quality. Great job on this new module!

test/view/selection.test.ts (1)

2-2: LGTM: Import statement updated correctly.

The import statement has been properly updated to include the new pathOf function alongside offsetOf. This change aligns with the introduction of the new pathOf test suite.

src/view/view.ts (3)

1-1: LGTM: Import statements updated correctly.

The new imports align with the changes in the handleBeforeInput method and the addition of the getSelection method. This ensures that all necessary functions and types are available for use in the View class.

Also applies to: 4-5


53-54: LGTM: Simplified input handling logic.

The handleBeforeInput method has been refactored to directly call insertText and splitBlock commands, which simplifies the code and improves maintainability. The use of this.notify() ensures that these commands are properly propagated through the Observable pattern.

Also applies to: 59-59


80-82: LGTM: New getSelection method added.

The new getSelection method enhances the View class by providing a way to retrieve the current selection from the container. This addition is well-aligned with the class's responsibilities and improves its overall functionality.

test/model/nodepos.test.ts (7)

1-5: LGTM: Import statements are well-organized and relevant.

The import statements are logically organized, with external imports (vitest) followed by local imports. All imported items appear to be relevant to the tests in this file.


7-16: LGTM: Path to Pos conversion test is well-structured.

This test effectively verifies the toNodePos function by converting a sample XML structure and asserting the correct node and offset. The use of a multi-level XML structure ensures the function works correctly with nested elements.


18-23: LGTM: Node to path conversion test is concise and effective.

This test verifies the pathOf function by converting a text node back to its path representation. The test uses a simpler XML structure, which is appropriate for this specific test case.


26-37: LGTM: Comprehensive test for iterating over nodes between positions.

This test effectively verifies the nodesBetween function by iterating over multiple paragraphs and asserting the correct XML output. The use of tokenToXML for comparison is a good approach.


39-51: LGTM: Test for iterating over a single text node is well-implemented.

This test verifies the behavior of nodesBetween when operating on a single text node with specific start and end offsets. The assertion correctly checks for the expected substring.


53-62: LGTM: Edge case test for identical start and end positions.

This test correctly verifies that nodesBetween returns an empty result when the start and end positions are the same. This is an important edge case to cover.


1-63: Excellent test coverage and structure for node position functionality.

This test file provides comprehensive coverage for the node position and iteration functionality. The tests are well-structured, covering various scenarios including conversions between paths and positions, iterating over multiple nodes, handling single text nodes, and edge cases. The use of sample XML structures and the tokenToXML function for comparisons is effective and enhances the readability of the tests.

Some points of note:

  1. Good separation of concerns with two distinct test suites.
  2. Effective use of the Vitest testing framework.
  3. Clear and descriptive test names.
  4. Appropriate use of helper functions (e.g., plainTextSchema.fromXML, tokenToXML).

Overall, this test file significantly contributes to the reliability of the node position and iteration functionality in the codebase.

src/model/nodes.ts (5)

1-7: LGTM: Import and TagType definition are clear and appropriate.

The import statement includes necessary types, and the TagType definition is well-documented and correctly defined as a union type.


27-41: LGTM: pathOf function is well-implemented.

The pathOf function efficiently builds the path from a node to its container. It includes appropriate error handling and uses an efficient method (unshift) to construct the path.


43-50: LGTM: lengthOf function is concise and correct.

The lengthOf function correctly handles both text and element nodes, providing an appropriate length measure for each type.


77-93: LGTM: insertBefore function is well-implemented.

The insertBefore function correctly inserts new nodes before a given node, efficiently handling multiple node insertion. It includes appropriate error handling and updates the parent of the inserted nodes.


1-129: Overall: Well-implemented node manipulation utilities with minor improvement opportunities.

The src/model/nodes.ts file provides a comprehensive set of utilities for working with a tree-like node structure. The implementation is generally solid, with appropriate error handling and efficient algorithms.

Key strengths:

  1. Clear type definitions and well-documented functions.
  2. Efficient implementations for node traversal and manipulation.
  3. Appropriate error handling in most functions.

Suggestions for improvement:

  1. Consider handling element attributes in the toXML function.
  2. Modify the splitText function to return both resulting nodes.
  3. Refactor insertBefore and insertAfter to reduce code duplication.
  4. Update the removed node's parent reference in the removeNode function.

These minor improvements would enhance the robustness and maintainability of the code. Overall, this is a well-structured and implemented set of utilities.

src/view/selection.ts (3)

1-2: LGTM: Import statements and type definition are appropriate.

The updated import statements and the new Position type definition are well-structured and properly documented. These changes align with the new functionality being introduced in the file.

Also applies to: 4-8


34-73: LGTM: pathOf function is well-implemented, but consider optimizations.

The pathOf function correctly calculates the path of a node within a container, handling the grouping of adjacent text nodes and traversing the DOM tree upwards.

Consider the following optimizations:

  1. Cache the result of Array.from(parent.childNodes) if it's used multiple times.
  2. The offset calculation for text nodes (lines 60-65) could potentially be optimized. Consider using a more efficient algorithm if dealing with a large number of text nodes.

Example of caching childNodes:

const childNodesArray = Array.from(parent.childNodes);
const groups = groupBy(
  childNodesArray,
  (p, c) => p.nodeType === Node.TEXT_NODE && c.nodeType === Node.TEXT_NODE,
);

Line range hint 1-121: LGTM: Overall file structure and consistency are excellent.

The file structure is logical and consistent, with imports at the top, followed by type definitions and well-documented functions. The new additions integrate seamlessly with the existing code, maintaining a high level of code quality and readability.

src/model/model.ts (5)

2-18: LGTM: Import statements and class declaration updated appropriately.

The new imports and the change in the Observable generic type align well with the transition to an operation-based model structure. This provides a solid foundation for the enhanced functionality of the Model class.


34-40: LGTM: createText and toXML methods implemented correctly.

These methods provide essential functionality for working with the new model structure. The createText method ensures that text nodes are created according to the schema, while toXML allows for easy conversion of the model back to XML format.


112-154: LGTM: nodePosOf and getContentEndRange methods implemented correctly.

Both methods are well-implemented and provide essential functionality for working with the model structure:

  • nodePosOf correctly handles the conversion of a path to a node position, with the option to split text nodes.
  • getContentEndRange efficiently traverses the tree to find the end of the content.

The implementation is clear and should work well with the new model structure.


157-158: Implement the 'move' method as previously noted.

As mentioned in a previous review comment, the move method is not implemented. This will cause runtime errors when applying 'move' operations.

Would you like assistance in implementing the move method? I can provide a basic structure or pseudocode to help you get started.


42-56: ⚠️ Potential issue

Implement 'move' operation and refine error handling.

The apply method structure is good, allowing for easy addition of new operation types. However, there are two issues to address:

  1. The 'move' operation is not implemented. This could lead to unexpected behavior when applying 'move' operations.

  2. The error message in the default case exposes the entire op object, which could potentially leak sensitive information.

To address these issues:

  1. Implement the 'move' operation or add a TODO comment if it's planned for future implementation.

  2. Modify the error message to only include the operation type:

- throw new Error(`invalid operation type: ${op}`);
+ throw new Error(`Invalid operation type: ${op.type}`);

Note: The error message issue was previously flagged in an earlier review. Please address this in your next revision.

Comment on lines +118 to +121
export function toIndexRange(
range: AbstractRange,
container: Node,
): IndexRange {
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Action Required: toRange function is still in use.

Multiple instances of toRange were found in the codebase:

  • src/view/view.ts
  • src/view/selection.ts

Please update all remaining usages of toRange to toIndexRange.

🔗 Analysis chain

LGTM: toIndexRange function renamed and updated correctly.

The renaming of toRange to toIndexRange and the change in return type to IndexRange accurately reflect the function's purpose.

Please ensure that all usages of the former toRange function returning an IndexRange are updated to use toIndexRange. Run the following script to check for any remaining usages:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find all occurrences of `toRange` in TypeScript files.

rg --type typescript --word-regexp '\btoRange\b'

Length of output: 85


Script:

#!/bin/bash
# Description: Find all occurrences of `toRange` in TypeScript files.

rg --glob "*.ts*" --word-regexp '\btoRange\b'

Length of output: 608

@hackerwins hackerwins merged commit 9f4613b into main Oct 19, 2024
3 checks passed
@hackerwins hackerwins deleted the schema branch October 19, 2024 12:19
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.

1 participant