1+ import type {
2+ // PositionPlainObject,
3+ SelectionPlainObject ,
4+ SerializedMarks ,
5+ TargetPlainObject ,
6+ } from "@cursorless/common" ;
7+
8+ import type { DecorationItem } from "shiki"
9+
10+ /**
11+ * Splits a string into an array of objects containing the line content
12+ * and the cumulative offset from the start of the string.
13+ *
14+ * @param {string } documentContents - The string to split into lines.
15+ * @returns {{ line: string, offset: number }[] } An array of objects with line content and cumulative offset.
16+ */
17+ function splitDocumentWithOffsets ( documentContents : string ) : { line : string ; offset : number } [ ] {
18+ const lines = documentContents . split ( "\n" ) ;
19+ let cumulativeOffset = 0 ;
20+
21+ return lines . map ( ( line ) => {
22+ const result = { line, offset : cumulativeOffset } ;
23+ cumulativeOffset += line . length + 1 ; // +1 for the newline character
24+ return result ;
25+ } ) ;
26+ }
27+
28+ /**
29+ * Creates decorations based on the split document with offsets and marks.
30+ *
31+ * @param {Object } options - An object containing optional fields like marks, command, or ide.
32+ * @param {SerializedMarks } [options.marks] - An object containing marks with line, start, and end positions.
33+ * @param {any } [options.command] - (Optional) The command object specifying the command details.
34+ * @param {any } [options.ide] - (Optional) The ide object specifying the IDE details.
35+ * @returns {DecorationItem[] } An array of decoration objects.
36+ */
37+ function createDecorations (
38+ options : {
39+ marks ?: SerializedMarks ;
40+ command ?: any ;
41+ ide ?: any ;
42+ lines ?: string [ ]
43+ selections ?: SelectionPlainObject [ ]
44+ thatMark ?: TargetPlainObject [ ]
45+ } = { } // Default to an empty object
46+ ) : DecorationItem [ ] {
47+ const { marks, ide, lines, selections, thatMark /* command */ } = options
48+ if ( thatMark ) {
49+ console . log ( "📅" , thatMark )
50+ }
51+
52+ const decorations : DecorationItem [ ] = [ ] ;
53+ const markDecorations = getMarkDecorations ( { marks, lines } )
54+ const ideFlashDecorations = getIdeFlashDecorations ( { lines, ide } )
55+ decorations . push ( ...markDecorations ) ;
56+ decorations . push ( ...ideFlashDecorations ) ;
57+ const selectionRanges = getSlections ( { selections } )
58+ decorations . push ( ...selectionRanges ) ;
59+
60+ if ( thatMark ) {
61+ const modificationReferences = getThatMarks ( { thatMark } )
62+ decorations . push ( ...modificationReferences ) ;
63+ }
64+
65+ return decorations
66+
67+ }
68+
69+ /**
70+ * Generates Shiki decorations for marks on a specific line.
71+ *
72+ * @param {Object } params - The parameters for generating decorations.
73+ * @param {SerializedMarks } [params.marks] - An object containing serialized marks with start and end positions.
74+ * @param {number } params.index - The index of the current line being processed.
75+ * @param {{ line: string; offset: number } } params.lineData - The line content and its cumulative offset.
76+ * @returns {DecorationItem[] } An array of Shiki decorations for the specified line.
77+ *
78+ */
79+ function getMarkDecorations ( {
80+ marks,
81+ lines
82+ } : {
83+ marks ?: SerializedMarks ;
84+ lines ?: string [ ]
85+ } ) : DecorationItem [ ] {
86+ const decorations : DecorationItem [ ] = [ ] ;
87+
88+ Object . entries ( marks || { } ) . forEach ( ( [ key , { start, end } ] ) => {
89+ const [ hatType , letter ] = key . split ( "." ) as [ keyof typeof classesMap , string ] ;
90+ console . log ( "🔑" , key , start , end ) ;
91+
92+ const markLineStart = start . line
93+
94+ if ( ! lines ) {
95+ console . warn ( "Lines are undefined. Skipping decoration generation." ) ;
96+ return [ ] ;
97+ }
98+ const currentLine = lines [ markLineStart ]
99+
100+ const searchStart = start . character ;
101+ const nextLetterIndex = currentLine . indexOf ( letter , searchStart ) ;
102+
103+ if ( nextLetterIndex === - 1 ) {
104+ console . warn (
105+ `Letter "${ letter } " not found after position ${ searchStart } in line: "${ currentLine } "`
106+ ) ;
107+ return ; // Skip this mark if the letter is not found
108+ }
109+
110+ const decorationItem : DecorationItem = {
111+ start,
112+ end : { line : start . line , character : nextLetterIndex + 1 } ,
113+ properties : {
114+ class : getDecorationClass ( hatType ) , // Replace with the desired class name for marks
115+ } ,
116+ alwaysWrap : true ,
117+ }
118+
119+ console . log ( "🔑🔑" , decorationItem )
120+
121+ decorations . push ( decorationItem ) ;
122+ } ) ;
123+
124+ return decorations ;
125+ }
126+
127+ type LineRange = { type : string ; start : number ; end : number }
128+ type PositionRange = { type : string ; start : { line : number ; character : number } ; end : { line : number ; character : number } } ;
129+
130+ type RangeType =
131+ | LineRange
132+ | PositionRange
133+
134+
135+ function getIdeFlashDecorations ( {
136+ ide,
137+ lines,
138+ } : {
139+ ide ?: {
140+ flashes ?: { style : keyof typeof classesMap ; range : RangeType } [ ] ;
141+ } ;
142+ lines ?: string [ ] ;
143+ } = { } ) : DecorationItem [ ] {
144+ if ( ! lines ) {
145+ console . warn ( "Lines are undefined. Skipping line decorations." ) ;
146+ return [ ] ;
147+ }
148+
149+ if ( ! ide ?. flashes || ! Array . isArray ( ide . flashes ) ) {
150+ console . warn ( "No flashes found in IDE. Skipping line decorations." ) ;
151+ return [ ] ;
152+ }
153+
154+ const decorations : DecorationItem [ ] = [ ] ;
155+
156+ const { flashes } = ide
157+
158+ flashes . forEach ( ( { style, range } ) => {
159+ const { type } = range ;
160+
161+ if ( isLineRange ( range ) ) {
162+ const { start : lineStart , end : lineEnd } = range
163+
164+ /* Split a multi-line range into single lines so that shiki doesn't add
165+ * multiple classes to the same span causing CSS conflicts
166+ */
167+ for ( let line = lineStart ; line <= lineEnd ; line ++ ) {
168+ const contentLine = lines [ line ] ;
169+ const startPosition = { line, character : 0 } ;
170+ const endPosition = { line, character : contentLine . length } ;
171+ const decorationItem = {
172+ start : startPosition ,
173+ end : endPosition ,
174+ properties : {
175+ class : `${ getDecorationClass ( style ) } full` ,
176+ } ,
177+ alwaysWrap : true ,
178+ } ;
179+ console . log ( "🔥🔥" , decorationItem ) ;
180+ decorations . push ( decorationItem ) ;
181+ }
182+
183+ } else if ( isPositionRange ( range ) ) {
184+ const { start : rangeStart , end : rangeEnd } = range ;
185+ const decorationItem = {
186+ start : rangeStart ,
187+ end : rangeEnd ,
188+ properties : {
189+ class : getDecorationClass ( style ) ,
190+ } ,
191+ alwaysWrap : true ,
192+ }
193+ console . log ( "🔥🔥" , decorationItem )
194+ decorations . push ( decorationItem ) ;
195+ } else {
196+ console . warn ( `Unknown range type "${ type } ". Skipping this flash.` ) ;
197+ }
198+ } ) ;
199+
200+ return decorations ;
201+ }
202+
203+ function getSlections (
204+ {
205+ selections,
206+ } : {
207+ selections ?: SelectionPlainObject [ ] ;
208+ lines ?: string [ ]
209+ } ) : DecorationItem [ ] {
210+ const decorations : DecorationItem [ ] = [ ] ;
211+ if ( selections === undefined || selections . length === 0 ) {
212+ console . warn ( "Lines are undefined. Skipping decoration generation." ) ;
213+ return [ ]
214+ }
215+ selections . forEach ( ( { anchor, active } ) => {
216+ const decorationItem = {
217+ start : anchor ,
218+ end : active ,
219+ properties : {
220+ class : getDecorationClass ( "selection" ) ,
221+ } ,
222+ alwaysWrap : true ,
223+ }
224+ decorations . push ( decorationItem )
225+ console . log ( "🟦" , decorationItem )
226+ } )
227+
228+ return decorations
229+ }
230+
231+ function getThatMarks ( { thatMark } : { thatMark : TargetPlainObject [ ] } ) : DecorationItem [ ] {
232+ console . log ( "☝️" , thatMark )
233+ const decorations : DecorationItem [ ] = [ ] ;
234+ if ( thatMark === undefined ) {
235+ console . warn ( "thatMarks are undefined. Skipping decoration generation." ) ;
236+ return [ ]
237+ }
238+
239+ return decorations
240+ }
241+
242+ // Type guard for line range
243+ function isLineRange ( range : RangeType ) : range is LineRange {
244+ return typeof range . start === "number"
245+ && typeof range . end === "number"
246+ && range . type === "line" ;
247+ }
248+
249+ // Type guard for position range
250+ function isPositionRange (
251+ range : RangeType
252+ ) : range is PositionRange {
253+ return typeof range . start === "object"
254+ && typeof range . end === "object"
255+ && range . type === "character" ;
256+ }
257+
258+
259+ const DEFAULT_HAT_CLASS = "hat default" ;
260+ const classesMap = {
261+ default : DEFAULT_HAT_CLASS ,
262+ pendingDelete : "decoration pendingDeleteBackground" ,
263+ referenced : "decoration referencedBackground" ,
264+ selection : "selection"
265+ } ;
266+
267+ function getDecorationClass ( key : keyof typeof classesMap ) : string {
268+ return classesMap [ key ] || DEFAULT_HAT_CLASS ;
269+ }
270+
271+
272+ export {
273+ splitDocumentWithOffsets ,
274+ createDecorations
275+ }
0 commit comments