Skip to content

Commit 75e770d

Browse files
authored
Add .jsonl syntax highlighting support to TextViewer (#1906)
1 parent 129fb0d commit 75e770d

4 files changed

Lines changed: 261 additions & 3 deletions

File tree

QuickLook.Plugin/QuickLook.Plugin.TextViewer/TextViewerPanel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,9 @@ private void Viewer_KeyDown(object sender, KeyEventArgs e)
163163
// RTL: Ctrl + RShift
164164
// LTR: Ctrl + LShift
165165
if (Keyboard.IsKeyDown(Key.RightShift))
166-
FlowDirection = System.Windows.FlowDirection.RightToLeft;
166+
FlowDirection = FlowDirection.RightToLeft;
167167
else if (Keyboard.IsKeyDown(Key.LeftShift))
168-
FlowDirection = System.Windows.FlowDirection.LeftToRight;
168+
FlowDirection = FlowDirection.LeftToRight;
169169
}
170170
else if (Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt))
171171
{
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright © 2017-2026 QL-Win Contributors
2+
//
3+
// This file is part of QuickLook program.
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
using ICSharpCode.AvalonEdit.Highlighting;
19+
using ICSharpCode.AvalonEdit.Rendering;
20+
using QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions.Light;
21+
using System.Collections.Generic;
22+
23+
namespace QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions.Dark;
24+
25+
public class JSONLHighlightingDefinition : DarkHighlightingDefinition
26+
{
27+
public override string Name => "JSONL";
28+
29+
public override string Extension => ".jsonl";
30+
31+
public override HighlightingRuleSet MainRuleSet => new();
32+
33+
public override HighlightingColor GetNamedColor(string name) => null;
34+
35+
public override IEnumerable<HighlightingColor> NamedHighlightingColors => [];
36+
37+
public override DocumentColorizingTransformer[] LineTransformers { get; } =
38+
[
39+
new JsonLineHighlighter(
40+
keyColor: "#9CDCF0", // field names (VSCode default)
41+
stringColor: "#CE9178", // string values (VSCode default)
42+
numberColor: "#B5CEA8", // numbers (VSCode default)
43+
boolNullColor:"#569CD6", // true/false/null (VSCode default)
44+
braceColor: "#DA66BE", // { }
45+
bracketColor: "#FFD710") // [ ]
46+
];
47+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright © 2017-2026 QL-Win Contributors
2+
//
3+
// This file is part of QuickLook program.
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
using ICSharpCode.AvalonEdit.Document;
19+
using ICSharpCode.AvalonEdit.Highlighting;
20+
using ICSharpCode.AvalonEdit.Rendering;
21+
using System.Collections.Generic;
22+
23+
namespace QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions.Light;
24+
25+
public class JSONLHighlightingDefinition : LightHighlightingDefinition
26+
{
27+
public override string Name => "JSONL";
28+
29+
public override string Extension => ".jsonl";
30+
31+
public override HighlightingRuleSet MainRuleSet => new();
32+
33+
public override HighlightingColor GetNamedColor(string name) => null;
34+
35+
public override IEnumerable<HighlightingColor> NamedHighlightingColors => [];
36+
37+
public override DocumentColorizingTransformer[] LineTransformers { get; } =
38+
[
39+
new JsonLineHighlighter(
40+
keyColor: "#0451A5", // field names (VSCode light default)
41+
stringColor: "#A31515", // string values (VSCode light default)
42+
numberColor: "#098658", // numbers (VSCode light default)
43+
boolNullColor:"#0000FF", // true/false/null (VSCode light default)
44+
braceColor: "#800000", // { }
45+
bracketColor: "#0451A5") // [ ]
46+
];
47+
}
48+
49+
/// <summary>
50+
/// Per-line JSON tokenizer for JSONL (JSON Lines) files.
51+
/// Each call to ColorizeLine processes one independent JSON object/array.
52+
/// </summary>
53+
public class JsonLineHighlighter(
54+
string keyColor,
55+
string stringColor,
56+
string numberColor,
57+
string boolNullColor,
58+
string braceColor,
59+
string bracketColor) : DocumentColorizingTransformer
60+
{
61+
protected override void ColorizeLine(DocumentLine line)
62+
{
63+
var text = CurrentContext.Document.GetText(line);
64+
if (string.IsNullOrWhiteSpace(text))
65+
return;
66+
67+
ColorizeJsonLine(line, text);
68+
}
69+
70+
private void ColorizeJsonLine(DocumentLine line, string text)
71+
{
72+
// Stack: true = currently inside an object (next unquoted string is a key),
73+
// false = currently inside an array (all strings are values).
74+
var contextStack = new Stack<bool>();
75+
bool expectKey = false;
76+
77+
int i = 0;
78+
while (i < text.Length)
79+
{
80+
char c = text[i];
81+
82+
if (char.IsWhiteSpace(c)) { i++; continue; }
83+
84+
switch (c)
85+
{
86+
case '{':
87+
Colorize(line, i, i + 1, braceColor);
88+
contextStack.Push(true);
89+
expectKey = true;
90+
i++;
91+
break;
92+
93+
case '}':
94+
Colorize(line, i, i + 1, braceColor);
95+
if (contextStack.Count > 0) contextStack.Pop();
96+
expectKey = false;
97+
i++;
98+
break;
99+
100+
case '[':
101+
Colorize(line, i, i + 1, bracketColor);
102+
contextStack.Push(false);
103+
expectKey = false;
104+
i++;
105+
break;
106+
107+
case ']':
108+
Colorize(line, i, i + 1, bracketColor);
109+
if (contextStack.Count > 0) contextStack.Pop();
110+
expectKey = false;
111+
i++;
112+
break;
113+
114+
case ':':
115+
// Colon separates key from value; next token is a value.
116+
expectKey = false;
117+
i++;
118+
break;
119+
120+
case ',':
121+
// In object context the next string is a key; in array it is a value.
122+
expectKey = contextStack.Count > 0 && contextStack.Peek();
123+
i++;
124+
break;
125+
126+
case '"':
127+
case '\'':
128+
i = TokenizeString(line, text, i, c, expectKey);
129+
if (expectKey)
130+
expectKey = false; // key consumed, colon follows next
131+
break;
132+
133+
default:
134+
if (c == '-' || char.IsDigit(c))
135+
{
136+
i = TokenizeNumber(line, text, i);
137+
expectKey = false;
138+
}
139+
else if (char.IsLetter(c))
140+
{
141+
i = TokenizeKeyword(line, text, i);
142+
expectKey = false;
143+
}
144+
else
145+
{
146+
i++;
147+
}
148+
break;
149+
}
150+
}
151+
}
152+
153+
private int TokenizeString(DocumentLine line, string text, int start, char quote, bool isKey)
154+
{
155+
int i = start + 1; // skip opening quote
156+
while (i < text.Length)
157+
{
158+
if (text[i] == '\\')
159+
{
160+
i += 2; // skip escape sequence
161+
continue;
162+
}
163+
if (text[i] == quote)
164+
{
165+
i++; // include closing quote
166+
break;
167+
}
168+
i++;
169+
}
170+
Colorize(line, start, i, isKey ? keyColor : stringColor);
171+
return i;
172+
}
173+
174+
private int TokenizeNumber(DocumentLine line, string text, int start)
175+
{
176+
int i = start;
177+
if (i < text.Length && text[i] == '-') i++; // optional leading minus
178+
while (i < text.Length && char.IsDigit(text[i])) i++;
179+
if (i < text.Length && text[i] == '.')
180+
{
181+
i++;
182+
while (i < text.Length && char.IsDigit(text[i])) i++;
183+
}
184+
if (i < text.Length && (text[i] == 'e' || text[i] == 'E'))
185+
{
186+
i++;
187+
if (i < text.Length && (text[i] == '+' || text[i] == '-')) i++;
188+
while (i < text.Length && char.IsDigit(text[i])) i++;
189+
}
190+
Colorize(line, start, i, numberColor);
191+
return i;
192+
}
193+
194+
private int TokenizeKeyword(DocumentLine line, string text, int start)
195+
{
196+
int i = start;
197+
while (i < text.Length && char.IsLetter(text[i])) i++;
198+
string keyword = text.Substring(start, i - start);
199+
if (keyword is "true" or "false" or "null")
200+
Colorize(line, start, i, boolNullColor);
201+
return i;
202+
}
203+
204+
private void Colorize(DocumentLine line, int from, int to, string hexColor)
205+
{
206+
ChangeLinePart(line.Offset + from, line.Offset + to, el =>
207+
{
208+
el.TextRunProperties.SetForegroundBrush(hexColor.ToBrush());
209+
});
210+
}
211+
}

SUPPORTED_FORMATS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Update not completed yet...
77
- `.rtf` (Rich Text Format)
88
- `.log` (Log file)
99
- `.ini` (Initialization/config file)
10-
- `.json` (JavaScript Object Notation)
10+
- `.json`, `.jsonl` (JavaScript Object Notation / JSON Lines)
1111
- `.xml` (Extensible Markup Language)
1212
- `.yaml`, `.yml` (YAML Ain't Markup Language)
1313
- `.md`, `.markdown`, `.mdx`, `.mmd`, `.mkd`, `.mdwn`, `.mdown`, `.mdc`, `.qmd`, `.rmd`, `.rmarkdown`, `.apib`, `.mdtxt`, `.mdtext` (Markdown and variants)

0 commit comments

Comments
 (0)