Skip to content
Open
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ This plugin is designed to display JSON strings in a Treeview format and highlig
5. Voila, that's it! If the JSON is valid, it will be displayed in a Treeview format.


## Format JSON Lines

Use **Plugins > JSON Viewer > Format JSON Lines** (directly below **Format JSON**) to pretty-print each JSON record in the selection, or the whole document when nothing is selected. Each nonblank input line must contain one complete JSON value. Blank lines are skipped.

The command uses the existing indentation, array formatting, line ending, comment, and trailing comma settings. It preserves record order and numeric precision, and retains a final newline if present. If any record is invalid, no text is replaced; the error identifies the input line and highlights the original error position.

The output contains pretty-printed JSON documents spanning multiple lines; it is no longer strict JSONL. The existing tree viewer and other commands still accept a single JSON document.

## Latest Updates:

### 2.2.0.0
Expand Down
2 changes: 2 additions & 0 deletions src/NppJsonViewer/Define.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum class CallBackID : int
{
SHOW_DOC_PANEL = 0,
FORMAT,
FORMAT_LINES,
COMPRESS,
SORT_BY_KEY,
SEP_1,
Expand All @@ -25,6 +26,7 @@ const TCHAR PLUGIN_CONFIG[] = TEXT("JSONViewer.ini");
const TCHAR TITLE_JSON_PANEL[] = TEXT("JSON Viewer");
const TCHAR MENU_SHOW_JSON_PANEL[] = TEXT("Show &JSON Viewer");
const TCHAR MENU_FORMAT_JSON[] = TEXT("&Format JSON");
const TCHAR MENU_FORMAT_JSON_LINES[] = TEXT("Format JSON &Lines");
const TCHAR MENU_COMPRESS_JSON[] = TEXT("&Compress JSON");
const TCHAR MENU_SORT_BY_KEY[] = TEXT("Sort by &key (ascending)");
const TCHAR MENU_SETTING[] = TEXT("&Settings");
Expand Down
47 changes: 47 additions & 0 deletions src/NppJsonViewer/JsonHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,53 @@ auto JsonHandler::FormatJson(const std::string& jsonText, LE le, LF lf, char ind
return ParseJson<flgBaseWriter>(jsonText, sb, handler);
}

auto JsonHandler::FormatJsonLines(const std::string& jsonText, LE le, LF lf, char indentChar, unsigned indentLen) -> const Result
{
const std::string eol = le == LE::kCrLf ? "\r\n" : le == LE::kCr ? "\r" : "\n";
std::string output;
size_t start = 0;
size_t lineNumber = 1;
bool hasRecord = false;

while (start < jsonText.size())
{
const size_t end = jsonText.find_first_of("\r\n", start);
const std::string line = jsonText.substr(start, end == std::string::npos ? end : end - start);
if (line.find_first_not_of(" \t") != std::string::npos)
{
auto result = FormatJson(line, le, lf, indentChar, indentLen);
if (!result.success)
{
result.error_pos += static_cast<int>(start);
result.error_str = "Line " + std::to_string(lineNumber) + ": " + result.error_str;
return result;
}
if (hasRecord)
output += eol;
output += result.response;
hasRecord = true;
}

if (end == std::string::npos)
break;
start = end + 1;
if (jsonText[end] == '\r' && start < jsonText.size() && jsonText[start] == '\n')
++start;
++lineNumber;
}

if (!hasRecord)
return FormatJson(jsonText, le, lf, indentChar, indentLen);

if (!jsonText.empty() && (jsonText.back() == '\r' || jsonText.back() == '\n'))
output += eol;

Result result;
result.success = true;
result.response = std::move(output);
return result;
}

auto JsonHandler::SortJsonByKey(const std::string& jsonText, LE le, LF lf, char indentChar, unsigned indentLen) -> const Result
{
auto res = ValidateJson(jsonText);
Expand Down
1 change: 1 addition & 0 deletions src/NppJsonViewer/JsonHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class JsonHandler

auto GetCompressedJson(const std::string& jsonText) -> const Result;
auto FormatJson(const std::string& jsonText, LE le, LF lf, char indentChar, unsigned indentLen) -> const Result;
auto FormatJsonLines(const std::string& jsonText, LE le, LF lf, char indentChar, unsigned indentLen) -> const Result;
auto SortJsonByKey(const std::string& jsonText, LE le, LF lf, char indentChar, unsigned indentLen) -> const Result;
auto ValidateJson(const std::string& jsonText) -> const Result;

Expand Down
30 changes: 30 additions & 0 deletions src/NppJsonViewer/JsonViewDlg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,36 @@ void JsonViewDlg::FormatJson()
ReDrawJsonTree();
}

void JsonViewDlg::FormatJsonLines()
{
UpdateTitle();
m_pEditor->RefreshViewHandle();

const auto selectedData = m_pEditor->GetJsonText();
const auto selectedText = IsSelectionValidJson(selectedData);
if (!selectedText.has_value() || selectedText.value().empty())
{
const std::wstring msg = IsMultiSelection(selectedData) ? JSON_ERR_MULTI_SELECTION : JSON_ERR_PARSE;
ShowMessage(JSON_INFO_TITLE, msg, MB_OK | MB_ICONINFORMATION);
return;
}

auto [le, lf, indentChar, indentLen] = GetFormatSetting();
const Result res = JsonHandler(m_pSetting->parseOptions).FormatJsonLines(selectedText.value(), le, lf, indentChar, indentLen);
if (res.success)
{
m_pEditor->ReplaceSelection(res.response);
HighlightAsJson();
}
else
{
ReportError(res);
}

// The tree viewer accepts a single JSON document, not a stream of records.
// Do not redraw it here: doing so would report a second, unrelated parse error.
}

void JsonViewDlg::CompressJson()
{
UpdateTitle();
Expand Down
1 change: 1 addition & 0 deletions src/NppJsonViewer/JsonViewDlg.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class JsonViewDlg

void ShowDlg(bool bShow);
void FormatJson();
void FormatJsonLines();
void CompressJson();
void SortJsonByKey();
void HandleTabActivated();
Expand Down
11 changes: 11 additions & 0 deletions src/NppJsonViewer/NppJsonPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ void NppJsonPlugin::InitCommandMenu()
m_shortcutCommands.SetShortCut(CallBackID::FORMAT, {true, true, true, 'M'});
m_shortcutCommands.SetCommand(CallBackID::FORMAT, MENU_FORMAT_JSON, Callback::FormatJson, false);

m_shortcutCommands.SetCommand(CallBackID::FORMAT_LINES, MENU_FORMAT_JSON_LINES, Callback::FormatJsonLines, false);

m_shortcutCommands.SetShortCut(CallBackID::COMPRESS, {true, true, true, 'C'});
m_shortcutCommands.SetCommand(CallBackID::COMPRESS, MENU_COMPRESS_JSON, Callback::CompressJson, false);

Expand Down Expand Up @@ -202,6 +204,15 @@ void NppJsonPlugin::FormatJson()
}
}

void NppJsonPlugin::FormatJsonLines()
{
ConstructJsonDlg();
if (m_pJsonViewDlg)
{
m_pJsonViewDlg->FormatJsonLines();
}
}

void NppJsonPlugin::CompressJson()
{
ConstructJsonDlg();
Expand Down
5 changes: 5 additions & 0 deletions src/NppJsonViewer/NppJsonPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ class NppJsonPlugin
{
m_pNppJsonPlugin->FormatJson();
}
static void FormatJsonLines()
{
m_pNppJsonPlugin->FormatJsonLines();
}
static void CompressJson()
{
m_pNppJsonPlugin->CompressJson();
Expand Down Expand Up @@ -81,6 +85,7 @@ class NppJsonPlugin

void ShowJsonDlg();
void FormatJson();
void FormatJsonLines();
void CompressJson();
void SortJsonByKey();
void OpenSettingDlg();
Expand Down
77 changes: 77 additions & 0 deletions tests/UnitTest/JsonLinesFormatTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#include <gtest/gtest.h>
#include "JsonHandler.h"

TEST(JsonLinesFormatTest, FormatsRecordsUsingExistingSettings)
{
JsonHandler handler({});
const auto result = handler.FormatJsonLines("{\"a\":1}\n{\"b\":[2,3]}", LE::kLf, LF::kFormatSingleLineArray, '\t', 1);
ASSERT_TRUE(result.success);
EXPECT_EQ(result.response, "{\n\t\"a\": 1\n}\n{\n\t\"b\": [2, 3]\n}");
}

TEST(JsonLinesFormatTest, HandlesLineEndingsAndFinalNewline)
{
JsonHandler handler({});
for (const auto& inputEol : {"\n", "\r\n", "\r"})
{
const std::string input = std::string("{}") + inputEol + "[]" + inputEol;
for (const auto& [ending, expectedEol] : {std::pair{LE::kLf, "\n"}, std::pair{LE::kCrLf, "\r\n"}, std::pair{LE::kCr, "\r"}})
{
const auto result = handler.FormatJsonLines(input, ending, {}, ' ', 2);
ASSERT_TRUE(result.success);
EXPECT_EQ(result.response, std::string("{}") + expectedEol + "[]" + expectedEol);
}
}
}

TEST(JsonLinesFormatTest, SkipsBlankLinesAndPreservesNumericText)
{
JsonHandler handler({});
const auto result = handler.FormatJsonLines("\n \t\r\n{\"n\":99999999999999999999}\n\n1e+10\n", LE::kLf, {}, ' ', 2);
ASSERT_TRUE(result.success);
EXPECT_EQ(result.response, "{\n \"n\": 99999999999999999999\n}\n1e+10\n");
}

TEST(JsonLinesFormatTest, SupportsAllRootValueTypes)
{
JsonHandler handler({});
const auto result = handler.FormatJsonLines("{}\n[]\n\"text\\nvalue\"\n42\ntrue\nfalse\nnull", LE::kLf, {}, ' ', 2);
ASSERT_TRUE(result.success);
EXPECT_EQ(result.response, "{}\n[]\n\"text\\nvalue\"\n42\ntrue\nfalse\nnull");
}

TEST(JsonLinesFormatTest, ReportsOriginalErrorOffsetWithoutPartialOutput)
{
JsonHandler handler({});
const std::string badLine = "{\"b\":}";
const std::string prefix = "{}\r\n\r\n";
const auto single = handler.FormatJson(badLine, LE::kLf, {}, ' ', 2);
const auto result = handler.FormatJsonLines(prefix + badLine + "\r\n{}", LE::kLf, {}, ' ', 2);
ASSERT_FALSE(result.success);
EXPECT_TRUE(result.response.empty());
EXPECT_EQ(result.error_pos, static_cast<int>(prefix.size()) + single.error_pos);
EXPECT_EQ(result.error_code, single.error_code);
EXPECT_EQ(result.error_str, "Line 3: " + single.error_str);
}

TEST(JsonLinesFormatTest, RejectsMultipleValuesOnOneLineAndMultilineRecords)
{
JsonHandler handler({});
EXPECT_FALSE(handler.FormatJsonLines("{} {}", LE::kLf, {}, ' ', 2).success);
EXPECT_FALSE(handler.FormatJsonLines("{\n\"a\":1\n}", LE::kLf, {}, ' ', 2).success);
EXPECT_FALSE(handler.FormatJsonLines(" \t\r\n", LE::kLf, {}, ' ', 2).success);
EXPECT_FALSE(handler.FormatJsonLines("", LE::kLf, {}, ' ', 2).success);
}

TEST(JsonLinesFormatTest, HonorsRelaxedParsingSettings)
{
ParseOptions options;
JsonHandler relaxed(options);
const std::string input = "{\"a\":1,}\n{/* comment */\"b\":2}";
EXPECT_TRUE(relaxed.FormatJsonLines(input, LE::kLf, {}, ' ', 2).success);
options.bIgnoreTrailingComma = false;
EXPECT_FALSE(JsonHandler(options).FormatJsonLines(input, LE::kLf, {}, ' ', 2).success);
options.bIgnoreTrailingComma = true;
options.bIgnoreComment = false;
EXPECT_FALSE(JsonHandler(options).FormatJsonLines(input, LE::kLf, {}, ' ', 2).success);
}
1 change: 1 addition & 0 deletions tests/UnitTest/UnitTest.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
<ClCompile Include="..\..\src\NppJsonViewer\Profile.cpp" />
<ClCompile Include="..\..\src\NppJsonViewer\RapidJsonHandler.cpp" />
<ClCompile Include="JsonCompressTest.cpp" />
<ClCompile Include="JsonLinesFormatTest.cpp" />
<ClCompile Include="JsonFormatTest.cpp" />
<ClCompile Include="JsonHandlerTest.cpp" />
<ClCompile Include="JsonSortByKeyTest.cpp" />
Expand Down
3 changes: 3 additions & 0 deletions tests/UnitTest/UnitTest.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
<ClCompile Include="..\..\src\NppJsonViewer\JsonHandler.cpp">
<Filter>Source Files\NppJsonViewer</Filter>
</ClCompile>
<ClCompile Include="JsonLinesFormatTest.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="JsonFormatTest.cpp">
<Filter>Source Files</Filter>
</ClCompile>
Expand Down