1- import { Fragment , Slice , type Node as ProseMirrorNode } from "@milkdown/kit/prose/model" ;
1+ import { Fragment , Mark , Slice , type Node as ProseMirrorNode } from "@milkdown/kit/prose/model" ;
22import type { EditorState , Selection , Transaction } from "@milkdown/kit/prose/state" ;
33
44import { isNonNullish } from "@/lib/predicates" ;
55
6- import {
7- getCandidateMarksAtSelection ,
8- getMarkRangeAtSelection ,
9- type ActiveMarkRange ,
10- } from "./marks" ;
6+ import { getCandidateMarksAtSelection , getMarkRangeAtSelection } from "./marks" ;
117import { isTextCaretSelection } from "./selections" ;
128import {
13- areProjectionMarksEqual ,
149 createProjectionMarkDescriptor ,
1510 createProjectionSource ,
1611 getProjectionDelimiterBounds ,
@@ -110,10 +105,14 @@ export interface SourceProjectionInsertionMatch {
110105 candidate : SourceProjectionInsertionCandidate ;
111106}
112107
113- interface ActiveProjectionRange extends ActiveMarkRange {
108+ interface ActiveProjectionRange extends TextRange {
114109 marks : ProjectionMarkDescriptor [ ] ;
115110}
116111
112+ interface ProjectionMarkSegment extends ActiveProjectionRange {
113+ documentMarks : readonly Mark [ ] ;
114+ }
115+
117116const createTextSlice = (
118117 state : EditorState ,
119118 text : string ,
@@ -190,8 +189,31 @@ const getActiveProjectionMarkRange = (state: EditorState): ActiveProjectionRange
190189 }
191190
192191 const candidateMarks = getCandidateMarksAtSelection ( state ) ;
192+ const linkType = state . schema . marks . link ;
193+ const activeLink = linkType
194+ ? ( candidateMarks . find ( ( mark ) => mark . type === linkType ) ?? null )
195+ : null ;
196+ const segments = getProjectionMarkSegments ( state ) ;
197+
198+ if ( activeLink ) {
199+ const linkRange = getMarkRangeAtSelection ( state , activeLink ) ;
200+
201+ if ( ! linkRange ) {
202+ return null ;
203+ }
204+
205+ const uniformLinkSegment = segments . find (
206+ ( segment ) => segment . from === linkRange . from && segment . to === linkRange . to ,
207+ ) ;
208+
209+ return uniformLinkSegment ? getActiveProjectionRange ( uniformLinkSegment ) : null ;
210+ }
193211
194212 for ( const markName of SUPPORTED_PROJECTION_MARK_NAMES ) {
213+ if ( markName === "link" ) {
214+ continue ;
215+ }
216+
195217 const markType = state . schema . marks [ markName ] ;
196218 if ( ! markType ) {
197219 continue ;
@@ -203,57 +225,75 @@ const getActiveProjectionMarkRange = (state: EditorState): ActiveProjectionRange
203225 continue ;
204226 }
205227
206- const range = getMarkRangeAtSelection ( state , activeMark ) ;
207- if ( ! range ) {
208- continue ;
209- }
210-
211- const marks = getProjectionMarksForRange ( state , range ) ;
228+ const segment = segments . find (
229+ ( { documentMarks, from, to } ) =>
230+ from <= selection . from && selection . from <= to && activeMark . isInSet ( documentMarks ) ,
231+ ) ;
212232
213- if ( marks ) {
214- return {
215- ...range ,
216- marks,
217- } ;
233+ if ( segment ) {
234+ return getActiveProjectionRange ( segment ) ;
218235 }
219236 }
220237
221238 return null ;
222239} ;
223240
224- const getProjectionMarksForRange = (
225- state : EditorState ,
226- range : ActiveMarkRange ,
227- ) : ProjectionMarkDescriptor [ ] | null => {
228- let supportedMarks : ProjectionMarkDescriptor [ ] | null = null ;
241+ const getActiveProjectionRange = ( { from , marks , to } : ProjectionMarkSegment ) => ( {
242+ from ,
243+ marks ,
244+ to ,
245+ } ) ;
229246
230- state . doc . nodesBetween ( range . from , range . to , ( node ) => {
231- if ( node . isText ) {
232- const projectionMarks = getProjectionMarksFromTextNode ( node ) ;
233-
234- if (
235- ! projectionMarks . length ||
236- ! projectionMarks . some ( ( mark ) => mark . markName === range . mark . type . name ) ||
237- ( supportedMarks && ! areProjectionMarksEqual ( supportedMarks , projectionMarks ) )
238- ) {
239- supportedMarks = null ;
240- return false ;
241- }
247+ const getProjectionMarkSegments = ( state : EditorState ) : ProjectionMarkSegment [ ] => {
248+ const { $from } = state . selection ;
249+
250+ if ( ! $from . parent . isTextblock ) {
251+ return [ ] ;
252+ }
242253
243- supportedMarks ??= projectionMarks ;
254+ const parentStart = $from . start ( ) ;
255+ const segments : ProjectionMarkSegment [ ] = [ ] ;
244256
245- return true ;
257+ $from . parent . forEach ( ( node , offset ) => {
258+ if ( ! node . isText ) {
259+ return ;
246260 }
247261
248- if ( node . isInline ) {
249- supportedMarks = null ;
250- return false ;
262+ const marks = getProjectionMarksFromTextNode ( node ) ;
263+
264+ if ( ! marks . length ) {
265+ return ;
251266 }
252267
253- return true ;
268+ const from = parentStart + offset ;
269+ const previousSegment = segments . at ( - 1 ) ;
270+
271+ if ( previousSegment ?. to === from && Mark . sameSet ( previousSegment . documentMarks , node . marks ) ) {
272+ previousSegment . to = from + node . nodeSize ;
273+ return ;
274+ }
275+
276+ segments . push ( {
277+ documentMarks : node . marks ,
278+ from,
279+ marks,
280+ to : from + node . nodeSize ,
281+ } ) ;
254282 } ) ;
255283
256- return supportedMarks ;
284+ return segments . flatMap ( ( segment ) => {
285+ if ( segment . marks . some ( ( mark ) => mark . markName === "link" ) ) {
286+ return [ segment ] ;
287+ }
288+
289+ const text = getRangeText ( state . doc , segment ) ;
290+ const leadingWhitespaceLength = / ^ \s + / u. exec ( text ) ?. [ 0 ] . length ?? 0 ;
291+ const trailingWhitespaceLength = / \s + $ / u. exec ( text ) ?. [ 0 ] . length ?? 0 ;
292+ const from = segment . from + leadingWhitespaceLength ;
293+ const to = segment . to - trailingWhitespaceLength ;
294+
295+ return from < to ? [ { ...segment , from, to } ] : [ ] ;
296+ } ) ;
257297} ;
258298
259299const getProjectionMarksFromTextNode = ( node : ProseMirrorNode ) : ProjectionMarkDescriptor [ ] => {
0 commit comments