Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ private void Viewer_KeyDown(object sender, KeyEventArgs e)
// RTL: Ctrl + RShift
// LTR: Ctrl + LShift
if (Keyboard.IsKeyDown(Key.RightShift))
FlowDirection = System.Windows.FlowDirection.RightToLeft;
FlowDirection = FlowDirection.RightToLeft;
else if (Keyboard.IsKeyDown(Key.LeftShift))
FlowDirection = System.Windows.FlowDirection.LeftToRight;
FlowDirection = FlowDirection.LeftToRight;
Comment on lines 165 to +168
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block now mixes unqualified FlowDirection.RightToLeft/LeftToRight with a fully-qualified System.Windows.FlowDirection.RightToLeft later in the same file (see the RTL auto-detect logic). Consider using one style consistently (either fully qualify here too, or rely on the using System.Windows; everywhere) to avoid confusion around the FlowDirection property vs enum type.

Copilot uses AI. Check for mistakes.
}
else if (Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright © 2017-2026 QL-Win Contributors
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Rendering;
using QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions.Light;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dark JSONL highlighting definition depends on the Light theme namespace solely to access JsonLineHighlighter (using ...HighlightingDefinitions.Light). This couples theme implementations together and complicates future refactors. Prefer placing shared transformers in a common HighlightingDefinitions namespace and referencing that from both themes.

Suggested change
using QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions.Light;
using QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions;

Copilot uses AI. Check for mistakes.
using System.Collections.Generic;

namespace QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions.Dark;

public class JSONLHighlightingDefinition : DarkHighlightingDefinition
{
public override string Name => "JSONL";

public override string Extension => ".jsonl";

public override HighlightingRuleSet MainRuleSet => new();

public override HighlightingColor GetNamedColor(string name) => null;

public override IEnumerable<HighlightingColor> NamedHighlightingColors => [];

public override DocumentColorizingTransformer[] LineTransformers { get; } =
[
new JsonLineHighlighter(
keyColor: "#9CDCF0", // field names (VSCode default)
stringColor: "#CE9178", // string values (VSCode default)
numberColor: "#B5CEA8", // numbers (VSCode default)
boolNullColor:"#569CD6", // true/false/null (VSCode default)
braceColor: "#DA66BE", // { }
bracketColor: "#FFD710") // [ ]
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright © 2017-2026 QL-Win Contributors
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Rendering;
using System.Collections.Generic;

namespace QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions.Light;

public class JSONLHighlightingDefinition : LightHighlightingDefinition
{
public override string Name => "JSONL";

public override string Extension => ".jsonl";

public override HighlightingRuleSet MainRuleSet => new();

public override HighlightingColor GetNamedColor(string name) => null;

public override IEnumerable<HighlightingColor> NamedHighlightingColors => [];

public override DocumentColorizingTransformer[] LineTransformers { get; } =
[
new JsonLineHighlighter(
keyColor: "#0451A5", // field names (VSCode light default)
stringColor: "#A31515", // string values (VSCode light default)
numberColor: "#098658", // numbers (VSCode light default)
boolNullColor:"#0000FF", // true/false/null (VSCode light default)
braceColor: "#800000", // { }
bracketColor: "#0451A5") // [ ]
];
}

/// <summary>
/// Per-line JSON tokenizer for JSONL (JSON Lines) files.
/// Each call to ColorizeLine processes one independent JSON object/array.
/// </summary>
public class JsonLineHighlighter(
string keyColor,
string stringColor,
string numberColor,
string boolNullColor,
string braceColor,
string bracketColor) : DocumentColorizingTransformer
{
Comment on lines +49 to +60
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonLineHighlighter is declared in the Light theme namespace/file but is also used by the Dark JSONL definition (via a using ...Light). This creates an odd cross-theme dependency and makes reuse harder. Consider moving JsonLineHighlighter into a theme-agnostic namespace/file (e.g., Themes/HighlightingDefinitions/JsonLineHighlighter.cs) and make it internal if it’s not part of the public API.

Copilot uses AI. Check for mistakes.
protected override void ColorizeLine(DocumentLine line)
{
var text = CurrentContext.Document.GetText(line);
if (string.IsNullOrWhiteSpace(text))
return;

ColorizeJsonLine(line, text);
}

private void ColorizeJsonLine(DocumentLine line, string text)
{
// Stack: true = currently inside an object (next unquoted string is a key),
// false = currently inside an array (all strings are values).
var contextStack = new Stack<bool>();
bool expectKey = false;

int i = 0;
while (i < text.Length)
{
char c = text[i];

if (char.IsWhiteSpace(c)) { i++; continue; }

switch (c)
{
case '{':
Colorize(line, i, i + 1, braceColor);
contextStack.Push(true);
expectKey = true;
i++;
break;

case '}':
Colorize(line, i, i + 1, braceColor);
if (contextStack.Count > 0) contextStack.Pop();
expectKey = false;
i++;
break;

case '[':
Colorize(line, i, i + 1, bracketColor);
contextStack.Push(false);
expectKey = false;
i++;
break;

case ']':
Colorize(line, i, i + 1, bracketColor);
if (contextStack.Count > 0) contextStack.Pop();
expectKey = false;
i++;
break;

case ':':
// Colon separates key from value; next token is a value.
expectKey = false;
i++;
break;

case ',':
// In object context the next string is a key; in array it is a value.
expectKey = contextStack.Count > 0 && contextStack.Peek();
i++;
break;

case '"':
case '\'':
i = TokenizeString(line, text, i, c, expectKey);
if (expectKey)
expectKey = false; // key consumed, colon follows next
break;

default:
if (c == '-' || char.IsDigit(c))
{
i = TokenizeNumber(line, text, i);
expectKey = false;
}
else if (char.IsLetter(c))
{
i = TokenizeKeyword(line, text, i);
expectKey = false;
}
else
{
i++;
}
break;
}
}
}

private int TokenizeString(DocumentLine line, string text, int start, char quote, bool isKey)
{
int i = start + 1; // skip opening quote
while (i < text.Length)
{
if (text[i] == '\\')
{
i += 2; // skip escape sequence
continue;
}
if (text[i] == quote)
{
i++; // include closing quote
break;
}
i++;
}
Colorize(line, start, i, isKey ? keyColor : stringColor);
return i;
Comment on lines +155 to +171
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TokenizeString can advance i past text.Length when a line ends with a trailing backslash (e.g., an incomplete escape sequence). That makes Colorize call ChangeLinePart with an end offset beyond the line end, which can throw. Clamp i to text.Length (or handle \ at end explicitly) before calling Colorize.

Copilot uses AI. Check for mistakes.
}

private int TokenizeNumber(DocumentLine line, string text, int start)
{
int i = start;
if (i < text.Length && text[i] == '-') i++; // optional leading minus
while (i < text.Length && char.IsDigit(text[i])) i++;
if (i < text.Length && text[i] == '.')
{
i++;
while (i < text.Length && char.IsDigit(text[i])) i++;
}
if (i < text.Length && (text[i] == 'e' || text[i] == 'E'))
{
i++;
if (i < text.Length && (text[i] == '+' || text[i] == '-')) i++;
while (i < text.Length && char.IsDigit(text[i])) i++;
}
Colorize(line, start, i, numberColor);
return i;
}

private int TokenizeKeyword(DocumentLine line, string text, int start)
{
int i = start;
while (i < text.Length && char.IsLetter(text[i])) i++;
string keyword = text.Substring(start, i - start);
if (keyword is "true" or "false" or "null")
Colorize(line, start, i, boolNullColor);
return i;
}
Comment on lines +194 to +202
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Avoid allocating a substring for keyword detection to reduce per-line allocations.

TokenizeKeyword currently calls text.Substring(start, i - start) for every keyword-like token, allocating a new string each time. Since you only need to recognize true, false, and null, you can compare length and characters directly on text (or use spans) to avoid these allocations and reduce GC pressure on large JSONL inputs.

Suggested change
private int TokenizeKeyword(DocumentLine line, string text, int start)
{
int i = start;
while (i < text.Length && char.IsLetter(text[i])) i++;
string keyword = text.Substring(start, i - start);
if (keyword is "true" or "false" or "null")
Colorize(line, start, i, boolNullColor);
return i;
}
private int TokenizeKeyword(DocumentLine line, string text, int start)
{
int i = start;
while (i < text.Length && char.IsLetter(text[i])) i++;
int length = i - start;
// Avoid substring allocations by checking expected keywords directly on the source text
bool isBoolOrNull =
(length == 4 && (
// "true"
(text[start] == 't' && text[start + 1] == 'r' && text[start + 2] == 'u' && text[start + 3] == 'e') ||
// "null"
(text[start] == 'n' && text[start + 1] == 'u' && text[start + 2] == 'l' && text[start + 3] == 'l')
))
||
(length == 5 &&
// "false"
text[start] == 'f' && text[start + 1] == 'a' && text[start + 2] == 'l' && text[start + 3] == 's' && text[start + 4] == 'e');
if (isBoolOrNull)
Colorize(line, start, i, boolNullColor);
return i;
}


private void Colorize(DocumentLine line, int from, int to, string hexColor)
{
ChangeLinePart(line.Offset + from, line.Offset + to, el =>
{
el.TextRunProperties.SetForegroundBrush(hexColor.ToBrush());
});
}
}
2 changes: 1 addition & 1 deletion SUPPORTED_FORMATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Update not completed yet...
- `.rtf` (Rich Text Format)
- `.log` (Log file)
- `.ini` (Initialization/config file)
- `.json` (JavaScript Object Notation)
- `.json`, `.jsonl` (JavaScript Object Notation / JSON Lines)
- `.xml` (Extensible Markup Language)
- `.yaml`, `.yml` (YAML Ain't Markup Language)
- `.md`, `.markdown`, `.mdx`, `.mmd`, `.mkd`, `.mdwn`, `.mdown`, `.mdc`, `.qmd`, `.rmd`, `.rmarkdown`, `.apib`, `.mdtxt`, `.mdtext` (Markdown and variants)
Expand Down
Loading