@lvce-editor/editor-worker 15.2.0 → 16.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2170,8 +2170,29 @@ const getStartDefaults = (tokens, minOffset) => {
2170
2170
  startIndex
2171
2171
  };
2172
2172
  };
2173
- const getLineInfoEmbeddedFull = (embeddedResults, tokenResults, line, normalize, tabSize, width, deltaX, averageCharWidth, minOffset, maxOffset) => {
2173
+ const getLineInfoEmbeddedFull = (embeddedResults, tokenResults, line, decorations, lineOffset, normalize, tabSize, width, deltaX, averageCharWidth, minOffset, maxOffset) => {
2174
2174
  const lineInfo = [];
2175
+
2176
+ // Build decoration map for this line (position -> decoration class)
2177
+ const decorationMap = new Map();
2178
+ for (let j = 0; j < decorations.length; j += 4) {
2179
+ const decorationOffset = decorations[j];
2180
+ const decorationLength = decorations[j + 1];
2181
+ const decorationType = decorations[j + 2];
2182
+ const relativeStart = decorationOffset - lineOffset;
2183
+ const relativeEnd = relativeStart + decorationLength;
2184
+
2185
+ // Only include decorations that overlap with this line
2186
+ if (relativeStart < line.length && relativeEnd > 0) {
2187
+ const decorationClassName = getDecorationClassName(decorationType);
2188
+ if (decorationClassName) {
2189
+ decorationMap.set(Math.max(0, relativeStart), {
2190
+ className: decorationClassName,
2191
+ end: Math.min(line.length, relativeEnd)
2192
+ });
2193
+ }
2194
+ }
2195
+ }
2175
2196
  const embeddedResult = embeddedResults[tokenResults.embeddedResultIndex];
2176
2197
  const embeddedTokens = embeddedResult.result.tokens;
2177
2198
  const embeddedTokenMap = embeddedResult.TokenMap;
@@ -2185,12 +2206,66 @@ const getLineInfoEmbeddedFull = (embeddedResults, tokenResults, line, normalize,
2185
2206
  for (let i = startIndex; i < tokensLength; i += 2) {
2186
2207
  const tokenType = embeddedTokens[i];
2187
2208
  const tokenLength = embeddedTokens[i + 1];
2188
- end += tokenLength;
2189
- const className = `Token ${embeddedTokenMap[tokenType] || 'Unknown'}`;
2190
- const text = line.slice(start, end);
2191
- const normalizedText = normalizeText(text, normalize, tabSize);
2192
- lineInfo.push(normalizedText, className);
2193
- start = end;
2209
+ const tokenEnd = start + tokenLength;
2210
+
2211
+ // Check if any decorations overlap with this token
2212
+ let hasOverlap = false;
2213
+ for (const [decorationStart, {
2214
+ end: decorationEnd
2215
+ }] of decorationMap) {
2216
+ if (decorationStart < tokenEnd && decorationEnd > start) {
2217
+ hasOverlap = true;
2218
+ break;
2219
+ }
2220
+ }
2221
+ if (hasOverlap) {
2222
+ // Token has decoration overlap - split into parts
2223
+ let currentPos = start;
2224
+ while (currentPos < tokenEnd) {
2225
+ // Find if current position is inside a decoration
2226
+ let activeDecoration = null;
2227
+ for (const [decorationStart, decoration] of decorationMap) {
2228
+ if (decorationStart <= currentPos && decoration.end > currentPos) {
2229
+ activeDecoration = decoration;
2230
+ break;
2231
+ }
2232
+ }
2233
+ if (activeDecoration) {
2234
+ // Render decorated part
2235
+ const partEnd = Math.min(tokenEnd, activeDecoration.end);
2236
+ const text = line.slice(currentPos, partEnd);
2237
+ const baseTokenClass = embeddedTokenMap[tokenType] || 'Unknown';
2238
+ const className = `Token ${baseTokenClass} ${activeDecoration.className}`;
2239
+ const normalizedText = normalizeText(text, normalize, tabSize);
2240
+ lineInfo.push(normalizedText, className);
2241
+ currentPos = partEnd;
2242
+ } else {
2243
+ // Find next decoration start or token end
2244
+ let nextDecorationStart = tokenEnd;
2245
+ for (const [decorationStart] of decorationMap) {
2246
+ if (decorationStart > currentPos && decorationStart < tokenEnd) {
2247
+ nextDecorationStart = Math.min(nextDecorationStart, decorationStart);
2248
+ }
2249
+ }
2250
+
2251
+ // Render non-decorated part
2252
+ const partEnd = nextDecorationStart;
2253
+ const text = line.slice(currentPos, partEnd);
2254
+ const className = `Token ${embeddedTokenMap[tokenType] || 'Unknown'}`;
2255
+ const normalizedText = normalizeText(text, normalize, tabSize);
2256
+ lineInfo.push(normalizedText, className);
2257
+ currentPos = partEnd;
2258
+ }
2259
+ }
2260
+ } else {
2261
+ // No decoration overlap - render token normally
2262
+ const text = line.slice(start, tokenEnd);
2263
+ const className = `Token ${embeddedTokenMap[tokenType] || 'Unknown'}`;
2264
+ const normalizedText = normalizeText(text, normalize, tabSize);
2265
+ lineInfo.push(normalizedText, className);
2266
+ }
2267
+ start = tokenEnd;
2268
+ end = tokenEnd;
2194
2269
  if (end >= maxOffset) {
2195
2270
  break;
2196
2271
  }
@@ -2223,11 +2298,25 @@ const getDifference = (start, averageCharWidth, deltaX) => {
2223
2298
  };
2224
2299
  const getLineInfoDefault = (line, tokenResults, embeddedResults, decorations, TokenMap, lineOffset, normalize, tabSize, width, deltaX, averageCharWidth, minOffset, maxOffset) => {
2225
2300
  const lineInfo = [];
2226
- let decorationIndex = 0;
2227
- for (; decorationIndex < decorations.length; decorationIndex += 3) {
2228
- const decorationOffset = decorations[decorationIndex];
2229
- if (decorationOffset >= lineOffset) {
2230
- break;
2301
+
2302
+ // Build decoration map for this line (position -> decoration class)
2303
+ const decorationMap = new Map();
2304
+ for (let j = 0; j < decorations.length; j += 4) {
2305
+ const decorationOffset = decorations[j];
2306
+ const decorationLength = decorations[j + 1];
2307
+ const decorationType = decorations[j + 2];
2308
+ const relativeStart = decorationOffset - lineOffset;
2309
+ const relativeEnd = relativeStart + decorationLength;
2310
+
2311
+ // Only include decorations that overlap with this line
2312
+ if (relativeStart < line.length && relativeEnd > 0) {
2313
+ const decorationClassName = getDecorationClassName(decorationType);
2314
+ if (decorationClassName) {
2315
+ decorationMap.set(Math.max(0, relativeStart), {
2316
+ className: decorationClassName,
2317
+ end: Math.min(line.length, relativeEnd)
2318
+ });
2319
+ }
2231
2320
  }
2232
2321
  }
2233
2322
  const {
@@ -2243,27 +2332,66 @@ const getLineInfoDefault = (line, tokenResults, embeddedResults, decorations, To
2243
2332
  for (let i = startIndex; i < tokensLength; i += 2) {
2244
2333
  const tokenType = tokens[i];
2245
2334
  const tokenLength = tokens[i + 1];
2246
- const decorationOffset = decorations[decorationIndex];
2247
- let extraClassName = '';
2248
- if (decorationOffset !== undefined && decorationOffset - lineOffset === start) {
2249
- // @ts-ignore
2250
- decorations[++decorationIndex];
2251
- const decorationType = decorations[++decorationIndex];
2252
- // @ts-ignore
2253
- decorations[++decorationIndex];
2254
- // decorationIndex,
2255
- // decorationLength,
2256
- // decorationType,
2257
- // decorationModifiers,
2258
- // })
2259
- extraClassName = getDecorationClassName(decorationType);
2335
+ const tokenEnd = start + tokenLength;
2336
+
2337
+ // Check if any decorations overlap with this token
2338
+ let hasOverlap = false;
2339
+ for (const [decorationStart, {
2340
+ end: decorationEnd
2341
+ }] of decorationMap) {
2342
+ if (decorationStart < tokenEnd && decorationEnd > start) {
2343
+ hasOverlap = true;
2344
+ break;
2345
+ }
2260
2346
  }
2261
- end += tokenLength;
2262
- const text = line.slice(start, end);
2263
- const className = `Token ${extraClassName || TokenMap[tokenType] || 'Unknown'}`;
2264
- const normalizedText = normalizeText(text, normalize, tabSize);
2265
- lineInfo.push(normalizedText, className);
2266
- start = end;
2347
+ if (hasOverlap) {
2348
+ // Token has decoration overlap - split into parts
2349
+ let currentPos = start;
2350
+ while (currentPos < tokenEnd) {
2351
+ // Find if current position is inside a decoration
2352
+ let activeDecoration = null;
2353
+ for (const [decorationStart, decoration] of decorationMap) {
2354
+ if (decorationStart <= currentPos && decoration.end > currentPos) {
2355
+ activeDecoration = decoration;
2356
+ break;
2357
+ }
2358
+ }
2359
+ if (activeDecoration) {
2360
+ // Render decorated part
2361
+ const partEnd = Math.min(tokenEnd, activeDecoration.end);
2362
+ const text = line.slice(currentPos, partEnd);
2363
+ const baseTokenClass = TokenMap[tokenType] || 'Unknown';
2364
+ const className = `Token ${baseTokenClass} ${activeDecoration.className}`;
2365
+ const normalizedText = normalizeText(text, normalize, tabSize);
2366
+ lineInfo.push(normalizedText, className);
2367
+ currentPos = partEnd;
2368
+ } else {
2369
+ // Find next decoration start or token end
2370
+ let nextDecorationStart = tokenEnd;
2371
+ for (const [decorationStart] of decorationMap) {
2372
+ if (decorationStart > currentPos && decorationStart < tokenEnd) {
2373
+ nextDecorationStart = Math.min(nextDecorationStart, decorationStart);
2374
+ }
2375
+ }
2376
+
2377
+ // Render non-decorated part
2378
+ const partEnd = nextDecorationStart;
2379
+ const text = line.slice(currentPos, partEnd);
2380
+ const className = `Token ${TokenMap[tokenType] || 'Unknown'}`;
2381
+ const normalizedText = normalizeText(text, normalize, tabSize);
2382
+ lineInfo.push(normalizedText, className);
2383
+ currentPos = partEnd;
2384
+ }
2385
+ }
2386
+ } else {
2387
+ // No decoration overlap - render token normally
2388
+ const text = line.slice(start, tokenEnd);
2389
+ const className = `Token ${TokenMap[tokenType] || 'Unknown'}`;
2390
+ const normalizedText = normalizeText(text, normalize, tabSize);
2391
+ lineInfo.push(normalizedText, className);
2392
+ }
2393
+ start = tokenEnd;
2394
+ end = tokenEnd;
2267
2395
  if (end >= maxOffset) {
2268
2396
  break;
2269
2397
  }
@@ -2281,7 +2409,7 @@ const getLineInfo$1 = (line, tokenResults, embeddedResults, decorations, TokenMa
2281
2409
  if (embeddedResults.length > 0 && tokenResults.embeddedResultIndex !== undefined) {
2282
2410
  const embeddedResult = embeddedResults[tokenResults.embeddedResultIndex];
2283
2411
  if (embeddedResult?.isFull) {
2284
- return getLineInfoEmbeddedFull(embeddedResults, tokenResults, line, normalize, tabSize, width, deltaX, averageCharWidth, minOffset, maxOffset);
2412
+ return getLineInfoEmbeddedFull(embeddedResults, tokenResults, line, decorations, lineOffset, normalize, tabSize, width, deltaX, averageCharWidth, minOffset, maxOffset);
2285
2413
  }
2286
2414
  }
2287
2415
  return getLineInfoDefault(line, tokenResults, embeddedResults, decorations, TokenMap, lineOffset, normalize, tabSize, width, deltaX, averageCharWidth, minOffset, maxOffset);
@@ -2302,10 +2430,25 @@ const getLineInfosViewport = (editor, tokens, embeddedResults, minLineY, maxLine
2302
2430
  for (let i = minLineY; i < maxLineY; i++) {
2303
2431
  const line = lines[i];
2304
2432
  const normalize = shouldNormalizeText(line);
2433
+
2434
+ // Use decorations that were pre-computed (includes links and diagnostics)
2435
+ // Filter decorations to only include those for this line
2436
+ const lineDecorations = [];
2437
+ for (let j = 0; j < decorations.length; j += 4) {
2438
+ const decorationOffset = decorations[j];
2439
+ const decorationLength = decorations[j + 1];
2440
+ const decorationType = decorations[j + 2];
2441
+ const decorationModifiers = decorations[j + 3];
2442
+
2443
+ // Include decoration if it starts within this line
2444
+ if (decorationOffset >= offset && decorationOffset < offset + line.length) {
2445
+ lineDecorations.push(decorationOffset, decorationLength, decorationType, decorationModifiers);
2446
+ }
2447
+ }
2305
2448
  const {
2306
2449
  difference,
2307
2450
  lineInfo
2308
- } = getLineInfo$1(line, tokens[i - minLineY], embeddedResults, decorations, tokenMap, offset, normalize, tabSize, width, deltaX, averageCharWidth);
2451
+ } = getLineInfo$1(line, tokens[i - minLineY], embeddedResults, lineDecorations, tokenMap, offset, normalize, tabSize, width, deltaX, averageCharWidth);
2309
2452
  result.push(lineInfo);
2310
2453
  differences.push(difference);
2311
2454
  offset += line.length + 1;
@@ -2336,7 +2479,7 @@ const getVisible$1 = async (editor, syncIncremental) => {
2336
2479
  tokenizersToLoad,
2337
2480
  tokens
2338
2481
  } = await getTokensViewport2(editor, minLineY, maxLineY, syncIncremental);
2339
- const minLineOffset = offsetAtSync(editor, minLineY, 0);
2482
+ const minLineOffset = await offsetAtSync(editor, minLineY, 0);
2340
2483
  const averageCharWidth = charWidth;
2341
2484
  const {
2342
2485
  differences,
@@ -3165,6 +3308,71 @@ const getLanguages = async (platform, assetDir) => {
3165
3308
  return languages;
3166
3309
  };
3167
3310
 
3311
+ /**
3312
+ * Gets all regex matches for a given text and regex pattern
3313
+ * @param text The text to match against
3314
+ * @param regex The regex pattern to use (should have global flag)
3315
+ * @returns Array of regex matches
3316
+ */
3317
+ const getRegexMatches = (text, regex) => {
3318
+ return [...text.matchAll(regex)];
3319
+ };
3320
+
3321
+ // URL matching regex pattern - matches common URL schemes
3322
+ // Supports: http://, https://, ftp://, ftps://, file://
3323
+ // Also matches URLs without explicit scheme (www.example.com)
3324
+ const URL_PATTERN = /(?:(?:https?|ftp|ftps|file):\/\/)?(?:www\.)?(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\/[^\s]*)?/g;
3325
+
3326
+ // Regex to check if URL has a scheme (http://, https://, ftp://, etc.)
3327
+ const HAS_SCHEME_PATTERN = /^(?:https?|ftp|ftps|file):\/\//;
3328
+
3329
+ // Regex to check if URL starts with www.
3330
+ const HAS_WWW_PATTERN = /^www\./;
3331
+
3332
+ /**
3333
+ * Detects links in a given text and returns their positions
3334
+ * @param text The text to scan for links
3335
+ * @returns Array of links with their start position and length
3336
+ */
3337
+ const detectLinks = text => {
3338
+ const matches = getRegexMatches(text, URL_PATTERN);
3339
+ const links = [];
3340
+ for (const match of matches) {
3341
+ const url = match[0];
3342
+ // Only consider as link if it has a scheme or starts with www.
3343
+ if (HAS_SCHEME_PATTERN.test(url) || HAS_WWW_PATTERN.test(url)) {
3344
+ links.push({
3345
+ length: url.length,
3346
+ start: match.index ?? 0
3347
+ });
3348
+ }
3349
+ }
3350
+ return links;
3351
+ };
3352
+
3353
+ /**
3354
+ * Detects all links in an editor and returns them as decorations
3355
+ * @param editor The editor containing lines to scan
3356
+ * @returns Flat array of decorations in format [offset, length, type, modifiers, ...]
3357
+ */
3358
+ const detectAllLinksAsDecorations = editor => {
3359
+ const decorations = [];
3360
+ const {
3361
+ lines
3362
+ } = editor;
3363
+ let offset = 0;
3364
+ for (const line of lines) {
3365
+ const links = detectLinks(line);
3366
+ for (const link of links) {
3367
+ const linkOffset = offset + link.start;
3368
+ // Add link decoration: offset, length, type, modifiers
3369
+ decorations.push(linkOffset, link.length, Link, 0);
3370
+ }
3371
+ offset += line.length + 1; // +1 for newline
3372
+ }
3373
+ return decorations;
3374
+ };
3375
+
3168
3376
  const measureCharacterWidth = async (fontWeight, fontSize, fontFamily, letterSpacing) => {
3169
3377
  return await measureTextWidth('a', fontWeight, fontSize, fontFamily, letterSpacing, false, 0);
3170
3378
  };
@@ -3271,11 +3479,15 @@ const updateDiagnostics = async newState => {
3271
3479
  if (!latest) {
3272
3480
  return newState;
3273
3481
  }
3274
- const decorations = await getVisibleDiagnostics(latest.newState, diagnostics);
3482
+ const visualDecorations = await getVisibleDiagnostics(latest.newState, diagnostics);
3483
+ // Re-detect link decorations after text changes
3484
+ const linkDecorations = detectAllLinksAsDecorations(latest.newState);
3275
3485
  const newEditor = {
3276
3486
  ...latest.newState,
3277
- decorations,
3278
- diagnostics
3487
+ decorations: linkDecorations,
3488
+ // Text-level decorations (flat array) for CSS classes
3489
+ diagnostics,
3490
+ visualDecorations // Visual decorations (objects with x, y, width, height) for squiggly underlines
3279
3491
  };
3280
3492
  set$6(newState.id, latest.oldState, newEditor);
3281
3493
  // @ts-ignore
@@ -3404,13 +3616,20 @@ const createEditor = async ({
3404
3616
  } else {
3405
3617
  newEditor3 = await setDeltaY$2(newEditor2, 0);
3406
3618
  }
3619
+
3620
+ // Detect links and initialize decorations
3621
+ const linkDecorations = detectAllLinksAsDecorations(newEditor3);
3622
+ const newEditor3WithLinks = {
3623
+ ...newEditor3,
3624
+ decorations: linkDecorations
3625
+ };
3407
3626
  const syncIncremental = getEnabled();
3408
3627
  const {
3409
3628
  differences,
3410
3629
  textInfos
3411
- } = await getVisible$1(newEditor3, syncIncremental);
3630
+ } = await getVisible$1(newEditor3WithLinks, syncIncremental);
3412
3631
  const newEditor4 = {
3413
- ...newEditor3,
3632
+ ...newEditor3WithLinks,
3414
3633
  differences,
3415
3634
  focus: FocusEditorText$1,
3416
3635
  focused: true,
@@ -8619,6 +8838,9 @@ const getLineInfos = (lines, tokenizer, languageId) => {
8619
8838
  lineInfos.push(lineInfo);
8620
8839
  currentLineState = result;
8621
8840
  }
8841
+ console.error({
8842
+ lineInfos
8843
+ });
8622
8844
  return lineInfos;
8623
8845
  };
8624
8846
 
@@ -10324,11 +10546,11 @@ const renderAdditionalFocusContext = {
10324
10546
  };
10325
10547
  const renderDecorations = {
10326
10548
  apply(oldState, newState) {
10327
- const dom = getDiagnosticsVirtualDom(newState.decorations);
10549
+ const dom = getDiagnosticsVirtualDom(newState.visualDecorations || []);
10328
10550
  return ['setDecorationsDom', dom];
10329
10551
  },
10330
10552
  isEqual(oldState, newState) {
10331
- return oldState.decorations === newState.decorations;
10553
+ return oldState.visualDecorations === newState.visualDecorations;
10332
10554
  }
10333
10555
  };
10334
10556
  const renderGutterInfo = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/editor-worker",
3
- "version": "15.2.0",
3
+ "version": "16.1.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git@github.com:lvce-editor/editor-worker.git"