Skip to content

Commit 98befaa

Browse files
committed
feat: Create helpers file for creating shiki decoration objects
1 parent a9332e0 commit 98befaa

File tree

1 file changed

+275
-0
lines changed

1 file changed

+275
-0
lines changed
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
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

Comments
 (0)