Skip to content

Commit 97c1958

Browse files
authored
feat(ai): expand selections to contain words (#2304)
* feat(ai): expand selection to words * ai unit test (wip) * fix tests
1 parent ebcb7ee commit 97c1958

File tree

44 files changed

+1070
-282
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1070
-282
lines changed

packages/core/src/api/blockManipulation/selections/selection.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { TextSelection, type Transaction } from "prosemirror-state";
22
import { TableMap } from "prosemirror-tables";
3-
43
import { Block } from "../../../blocks/defaultBlocks.js";
54
import { Selection } from "../../../editor/selectionTypes.js";
65
import {
@@ -9,6 +8,7 @@ import {
98
InlineContentSchema,
109
StyleSchema,
1110
} from "../../../schema/index.js";
11+
import { expandPMRangeToWords } from "../../../util/expandToWords.js";
1212
import { getBlockInfo, getNearestBlockPos } from "../../getBlockInfoFromPos.js";
1313
import {
1414
nodeToBlock,
@@ -220,12 +220,17 @@ export function setSelection(
220220
tr.setSelection(TextSelection.create(tr.doc, startPos, endPos));
221221
}
222222

223-
export function getSelectionCutBlocks(tr: Transaction) {
223+
export function getSelectionCutBlocks(tr: Transaction, expandToWords = false) {
224224
// TODO: fix image node selection
225225

226226
const pmSchema = getPmSchema(tr);
227-
let start = tr.selection.$from;
228-
let end = tr.selection.$to;
227+
228+
const range = expandToWords
229+
? expandPMRangeToWords(tr.doc, tr.selection)
230+
: tr.selection;
231+
232+
let start = range.$from;
233+
let end = range.$to;
229234

230235
// the selection moves below are used to make sure `prosemirrorSliceToSlicedBlocks` returns
231236
// the correct information about whether content is cut at the start or end of a block

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
} from "@tiptap/core";
88
import { type Command, type Plugin, type Transaction } from "@tiptap/pm/state";
99
import { Node, Schema } from "prosemirror-model";
10-
1110
import type { BlocksChanged } from "../api/getBlocksChangedByTransaction.js";
1211
import { blockToNode } from "../api/nodeConversions/blockToNode.js";
1312
import {
@@ -18,6 +17,8 @@ import {
1817
DefaultStyleSchema,
1918
PartialBlock,
2019
} from "../blocks/index.js";
20+
import type { CollaborationOptions } from "../extensions/Collaboration/Collaboration.js";
21+
import { BlockChangeExtension } from "../extensions/index.js";
2122
import { UniqueID } from "../extensions/tiptap-extensions/UniqueID/UniqueID.js";
2223
import type { Dictionary } from "../i18n/dictionary.js";
2324
import { en } from "../i18n/locales/index.js";
@@ -51,8 +52,6 @@ import {
5152
} from "./managers/index.js";
5253
import type { Selection } from "./selectionTypes.js";
5354
import { transformPasted } from "./transformPasted.js";
54-
import { BlockChangeExtension } from "../extensions/index.js";
55-
import type { CollaborationOptions } from "../extensions/Collaboration/Collaboration.js";
5655

5756
export type BlockCache<
5857
BSchema extends BlockSchema = any,
@@ -934,8 +933,8 @@ export class BlockNoteEditor<
934933
* If the selection starts / ends halfway through a block, the returned block will be
935934
* only the part of the block that is included in the selection.
936935
*/
937-
public getSelectionCutBlocks() {
938-
return this._selectionManager.getSelectionCutBlocks();
936+
public getSelectionCutBlocks(expandToWords = false) {
937+
return this._selectionManager.getSelectionCutBlocks(expandToWords);
939938
}
940939

941940
/**

packages/core/src/editor/managers/SelectionManager.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isNodeSelection, posToDOMRect } from "@tiptap/core";
12
import {
23
getSelection,
34
getSelectionCutBlocks,
@@ -7,21 +8,20 @@ import {
78
getTextCursorPosition,
89
setTextCursorPosition,
910
} from "../../api/blockManipulation/selections/textCursorPosition.js";
10-
import { isNodeSelection, posToDOMRect } from "@tiptap/core";
11+
import {
12+
DefaultBlockSchema,
13+
DefaultInlineContentSchema,
14+
DefaultStyleSchema,
15+
} from "../../blocks/defaultBlocks.js";
1116
import {
1217
BlockIdentifier,
1318
BlockSchema,
1419
InlineContentSchema,
1520
StyleSchema,
1621
} from "../../schema/index.js";
17-
import {
18-
DefaultBlockSchema,
19-
DefaultInlineContentSchema,
20-
DefaultStyleSchema,
21-
} from "../../blocks/defaultBlocks.js";
22-
import { Selection } from "../selectionTypes.js";
23-
import { TextCursorPosition } from "../cursorPositionTypes.js";
2422
import { BlockNoteEditor } from "../BlockNoteEditor.js";
23+
import { TextCursorPosition } from "../cursorPositionTypes.js";
24+
import { Selection } from "../selectionTypes.js";
2525

2626
export class SelectionManager<
2727
BSchema extends BlockSchema = DefaultBlockSchema,
@@ -47,8 +47,8 @@ export class SelectionManager<
4747
* If the selection starts / ends halfway through a block, the returned block will be
4848
* only the part of the block that is included in the selection.
4949
*/
50-
public getSelectionCutBlocks() {
51-
return this.editor.transact((tr) => getSelectionCutBlocks(tr));
50+
public getSelectionCutBlocks(expandToWords = false) {
51+
return this.editor.transact((tr) => getSelectionCutBlocks(tr, expandToWords));
5252
}
5353

5454
/**

packages/core/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export * from "./api/blockManipulation/commands/insertBlocks/insertBlocks.js";
22
export * from "./api/blockManipulation/commands/replaceBlocks/replaceBlocks.js";
3-
export * from "./api/blockManipulation/commands/updateBlock/updateBlock.js";
43
export * from "./api/blockManipulation/commands/replaceBlocks/util/fixColumnList.js";
4+
export * from "./api/blockManipulation/commands/updateBlock/updateBlock.js";
55
export * from "./api/exporters/html/externalHTMLExporter.js";
66
export * from "./api/exporters/html/internalHTMLSerializer.js";
77
export * from "./api/getBlockInfoFromPos.js";
@@ -19,6 +19,7 @@ export * from "./i18n/dictionary.js";
1919
export * from "./schema/index.js";
2020
export * from "./util/browser.js";
2121
export * from "./util/combineByGroup.js";
22+
export * from "./util/expandToWords.js";
2223
export * from "./util/string.js";
2324
export * from "./util/table.js";
2425
export * from "./util/typescript.js";
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { Node, ResolvedPos } from "prosemirror-model";
2+
3+
/**
4+
* Expands a range (start to end) to include the rest of the word if it starts or ends within a word
5+
*/
6+
export function expandPMRangeToWords(
7+
doc: Node,
8+
range: { $from: ResolvedPos; $to: ResolvedPos },
9+
) {
10+
let { $from, $to } = range;
11+
12+
// Expand Start
13+
// If the selection starts with a word character or punctuation, check if we need to expand left to include the rest of the word
14+
if ($from.pos > $from.start() && $from.pos < doc.content.size) {
15+
const charAfterStart = doc.textBetween($from.pos, $from.pos + 1);
16+
if (/^[\w\p{P}]$/u.test(charAfterStart)) {
17+
const textBefore = doc.textBetween($from.start(), $from.pos);
18+
const wordMatch = textBefore.match(/[\w\p{P}]+$/u);
19+
if (wordMatch) {
20+
$from = doc.resolve($from.pos - wordMatch[0].length);
21+
}
22+
}
23+
}
24+
25+
// Expand End
26+
// If the selection ends with a word characte or punctuation, check if we need to expand right to include the rest of the word
27+
if ($to.pos < $to.end() && $to.pos > 0) {
28+
const charBeforeEnd = doc.textBetween($to.pos - 1, $to.pos);
29+
if (/^[\w\p{P}]$/u.test(charBeforeEnd)) {
30+
const textAfter = doc.textBetween($to.pos, $to.end());
31+
const wordMatch = textAfter.match(/^[\w\p{P}]+/u);
32+
if (wordMatch) {
33+
$to = doc.resolve($to.pos + wordMatch[0].length);
34+
}
35+
}
36+
}
37+
return { $from, $to, from: $from.pos, to: $to.pos };
38+
}

packages/xl-ai/src/api/aiRequest/builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export async function buildAIRequest(opts: {
5151
: undefined;
5252

5353
const selectionInfo = useSelection
54-
? opts.editor.getSelectionCutBlocks()
54+
? opts.editor.getSelectionCutBlocks(true)
5555
: undefined;
5656

5757
const streamTools = streamToolsProvider.getStreamTools(

packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/anthropic.messages/claude-3-7-sonnet-latest (streaming)/add paragraph and update selection_1_039451748eb07d71d3d7f96c97950d62.json

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)