|
| 1 | +import * as vscode from 'vscode' |
| 2 | + |
| 3 | +// Maximum file size in bytes to process (1MB) |
| 4 | +const MAX_FILE_SIZE = 1024 * 1024 |
| 5 | + |
| 6 | +// Regex patterns for matching colors |
| 7 | +const COLOR_PATTERNS = { |
| 8 | + hex: /#([\dA-Fa-f]{3}){1,2}\b/g, |
| 9 | + rgb: /rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/g, |
| 10 | + rgba: /rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0|1|0?\.\d+)\s*\)/g |
| 11 | +} |
| 12 | + |
| 13 | +function parseHexColor(hex: string): vscode.Color | undefined { |
| 14 | + hex = hex.slice(1) // Remove # |
| 15 | + let r = 0; let g = 0; let b = 0 |
| 16 | + |
| 17 | + if (hex.length === 3) { |
| 18 | + r = Number.parseInt(hex[0]! + hex[0]!, 16) / 255 |
| 19 | + g = Number.parseInt(hex[1]! + hex[1]!, 16) / 255 |
| 20 | + b = Number.parseInt(hex[2]! + hex[2]!, 16) / 255 |
| 21 | + } else if (hex.length === 6) { |
| 22 | + r = Number.parseInt(hex.slice(0, 2), 16) / 255 |
| 23 | + g = Number.parseInt(hex.slice(2, 4), 16) / 255 |
| 24 | + b = Number.parseInt(hex.slice(4, 6), 16) / 255 |
| 25 | + } else { |
| 26 | + return undefined |
| 27 | + } |
| 28 | + |
| 29 | + return new vscode.Color(r, g, b, 1) |
| 30 | +} |
| 31 | + |
| 32 | +function parseRgbColor(match: string): vscode.Color | undefined { |
| 33 | + const values = match.match(/\d+/g) |
| 34 | + if (!values || values.length < 3) return undefined |
| 35 | + |
| 36 | + const rgbValues = values.slice(0, 3).map(v => { |
| 37 | + const parsed = Number.parseInt(v, 10) |
| 38 | + return Number.isNaN(parsed) ? undefined : parsed / 255 |
| 39 | + }) |
| 40 | + if (rgbValues.some(v => v === undefined || v < 0 || v > 1)) return undefined |
| 41 | + |
| 42 | + const alpha = values.length === 4 ? Number.parseFloat(values[3]!) : 1 |
| 43 | + return new vscode.Color(rgbValues[0]!, rgbValues[1]!, rgbValues[2]!, alpha) |
| 44 | +} |
| 45 | + |
| 46 | +export default (): vscode.Disposable => vscode.languages.registerColorProvider( |
| 47 | + [ |
| 48 | + { language: 'typescript' }, |
| 49 | + { language: 'javascript' }, |
| 50 | + { language: 'typescriptreact' }, |
| 51 | + { language: 'javascriptreact' }, |
| 52 | + ], |
| 53 | + { |
| 54 | + async provideDocumentColors( |
| 55 | + document: vscode.TextDocument, |
| 56 | + token: vscode.CancellationToken |
| 57 | + ): Promise<vscode.ColorInformation[]> { |
| 58 | + // Skip large files |
| 59 | + const textEncoder = new TextEncoder() |
| 60 | + const fileSize = textEncoder.encode(document.getText()).length |
| 61 | + if (fileSize > MAX_FILE_SIZE) { |
| 62 | + return [] |
| 63 | + } |
| 64 | + |
| 65 | + const text = document.getText() |
| 66 | + const colors: vscode.ColorInformation[] = [] |
| 67 | + |
| 68 | + // Match hex colors |
| 69 | + let match = COLOR_PATTERNS.hex.exec(text) |
| 70 | + while (match !== null) { |
| 71 | + const color = parseHexColor(match[0]) |
| 72 | + if (color) { |
| 73 | + const range = new vscode.Range( |
| 74 | + document.positionAt(match.index), |
| 75 | + document.positionAt(match.index + match[0].length) |
| 76 | + ) |
| 77 | + colors.push(new vscode.ColorInformation(range, color)) |
| 78 | + } |
| 79 | + |
| 80 | + match = COLOR_PATTERNS.hex.exec(text) |
| 81 | + } |
| 82 | + |
| 83 | + // Match rgb/rgba colors |
| 84 | + const rgbPatterns = [COLOR_PATTERNS.rgb, COLOR_PATTERNS.rgba] |
| 85 | + for (const pattern of rgbPatterns) { |
| 86 | + let rgbMatch = pattern.exec(text) |
| 87 | + while (rgbMatch !== null) { |
| 88 | + const color = parseRgbColor(rgbMatch[0]) |
| 89 | + if (color) { |
| 90 | + const range = new vscode.Range( |
| 91 | + document.positionAt(rgbMatch.index), |
| 92 | + document.positionAt(rgbMatch.index + rgbMatch[0].length) |
| 93 | + ) |
| 94 | + colors.push(new vscode.ColorInformation(range, color)) |
| 95 | + } |
| 96 | + |
| 97 | + rgbMatch = pattern.exec(text) |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + return colors |
| 102 | + }, |
| 103 | + |
| 104 | + provideColorPresentations( |
| 105 | + color: vscode.Color, |
| 106 | + context: { document: vscode.TextDocument, range: vscode.Range }, |
| 107 | + token: vscode.CancellationToken |
| 108 | + ): vscode.ProviderResult<vscode.ColorPresentation[]> { |
| 109 | + const red = Math.round(color.red * 255) |
| 110 | + const green = Math.round(color.green * 255) |
| 111 | + const blue = Math.round(color.blue * 255) |
| 112 | + |
| 113 | + const hex = `#${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}` |
| 114 | + |
| 115 | + const presentations = [ |
| 116 | + new vscode.ColorPresentation(hex), |
| 117 | + new vscode.ColorPresentation(`rgb(${red}, ${green}, ${blue})`) |
| 118 | + ] |
| 119 | + |
| 120 | + if (color.alpha !== 1) { |
| 121 | + presentations.push( |
| 122 | + new vscode.ColorPresentation(`rgba(${red}, ${green}, ${blue}, ${color.alpha.toFixed(2)})`) |
| 123 | + ) |
| 124 | + } |
| 125 | + |
| 126 | + return presentations |
| 127 | + } |
| 128 | + } |
| 129 | + ) |
0 commit comments