Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve EDN response #7777

Merged
merged 23 commits into from
Sep 4, 2024
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 @@ -52,6 +52,7 @@ import 'codemirror-graphql/variables/mode';
import './modes/nunjucks';
import './modes/curl';
import './modes/openapi';
import './modes/clojure';
import './lint/javascript-async-lint';
import './lint/json-lint';
import './extensions/autocomplete';
Expand Down
10 changes: 10 additions & 0 deletions packages/insomnia/src/ui/components/codemirror/code-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import './base-imports';

Check failure on line 1 in packages/insomnia/src/ui/components/codemirror/code-editor.tsx

View workflow job for this annotation

GitHub Actions / Test

Run autofix to sort these imports!

import classnames from 'classnames';
import clone from 'clone';
Expand Down Expand Up @@ -29,6 +29,7 @@
import { NunjucksModal } from '../modals/nunjucks-modal';
import { isKeyCombinationInRegistry } from '../settings/shortcuts';
import { normalizeIrregularWhitespace } from './normalizeIrregularWhitespace';
import { ednPrettify } from '../../../utils/prettify/edn';
const TAB_SIZE = 4;
const MAX_SIZE_FOR_LINTING = 1000000; // Around 1MB

Expand Down Expand Up @@ -237,6 +238,13 @@
return code;
}
};
const prettifyEDN = (code: string) => {
try {
return ednPrettify(code);
} catch (error) {
return code;
}
};
if (typeof code !== 'string') {
console.warn('Code editor was passed non-string value', code);
return;
Expand All @@ -248,6 +256,8 @@
code = prettifyXML(code, filter);
} else if (mode?.includes('json')) {
code = prettifyJSON(code, filter);
} else if (mode?.includes('edn')) {
code = prettifyEDN(code);
}
}
// this prevents codeMirror from needlessly setting the same thing repeatedly (which has the effect of moving the user's cursor and resetting the viewport scroll: a bad user experience)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import CodeMirror from 'codemirror';

CodeMirror.extendMode('clojure', { fold: 'brace' });
25 changes: 25 additions & 0 deletions packages/insomnia/src/utils/prettify/edn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from '@jest/globals';
import fs from 'fs';
import path from 'path';

import { ednPrettify } from './edn';

describe('ednPrettify()', () => {
const basePath = path.join(__dirname, './fixtures/edn');
const files = fs.readdirSync(basePath);
for (const file of files) {
if (!file.match(/-input\.edn$/)) {
continue;
}

const slug = file.replace(/-input\.edn$/, '');
const name = slug.replace(/-/g, ' ');

it(`handles ${name}`, () => {
const input = fs.readFileSync(path.join(basePath, `${slug}-input.edn`), 'utf8').trim();
const output = fs.readFileSync(path.join(basePath, `${slug}-output.edn`), 'utf8').trim();
const result = ednPrettify(input);
expect(result).toBe(output);
});
}
});
175 changes: 175 additions & 0 deletions packages/insomnia/src/utils/prettify/edn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
const delimitersData = [
// Procedence matters
['#{', '}'],
['{', '}'],
['[', ']'],
];

const [startDelimiters, endDelimiters] = delimitersData.reduce(
(acc, e) => {
acc[0].push(e[0]);
acc[1].push(e[1]);
return acc;
},
[[], []] as typeof delimitersData
);

function tokenize(edn: string) {
let insideString = false;

const tokens: string[] = [];

let symbol = '';

for (const c of edn) {
if (!insideString) {
// Ignore when
if (c === ',' || c === '\n' || c === '\r' || c === '\t' || (c === ' ')) {
if (symbol) {
tokens.push(symbol);
symbol = '';
}
continue;
} else if (c === '"') {
insideString = true;
symbol += c;
} else if (startDelimiters.includes(symbol + c)) {
tokens.push(symbol + c);
symbol = '';
} else if (endDelimiters.includes(c)) {
if (symbol) {
tokens.push(symbol);
}
tokens.push(c);
symbol = '';
} else {
symbol += c;
}
continue;
}

if (c === '"' && symbol.at(-1) !== '\\') {
insideString = false;
tokens.push(symbol + c);
symbol = '';
} else {
symbol += c;
}
}

return tokens;
}

function spacesOnLeft(spaces: number[]) {
const length = spaces.reduce((acc, e) => acc + e, 0);

return Array.from({ length })
.map(() => ' ')
.join('');
}

function tokensToLines(tokens: ReturnType<typeof tokenize>) {
const lines: string[][] = [];

const elements: { spaces: number; perLine: number }[] = [];

let currentLine: string[] = [];

let keyValue: string[] = [];

let tokenUsed = false;

for (const [i, t] of tokens.entries()) {
const nextToken = tokens.at(i + 1);
const nextEnding = nextToken && endDelimiters.includes(nextToken);

if (tokenUsed) {
const line = currentLine.join('');
// Check if its a empty structure, in that case, store current line and start a new one
if (!nextEnding && line.trim()) {
lines.push(currentLine);
currentLine = [spacesOnLeft(elements.map(e => e.spaces))];
}
tokenUsed = false;
continue;
}

const startDelimiter = startDelimiters.includes(t);
const endDelimiter = endDelimiters.includes(t);

if (startDelimiter) {
keyValue = [];
currentLine.push(t);
if (nextEnding) {
currentLine.push(nextToken);
tokenUsed = true;
} else {
const currenLineLength = currentLine.map(e => e.length).reduce((acc, e) => acc + e);
const spacesAlreadyCounted = elements.reduce((acc, e) => acc + e.spaces, 0);
elements.push({ spaces: currenLineLength - spacesAlreadyCounted, perLine: t === '{' ? 2 : 1 });
}
continue;
}

if (endDelimiter) {
keyValue = [];
if (currentLine.length > 1) {
currentLine.push(t);
lines.push(currentLine);
} else {
lines.at(-1)!.push(t);
}
elements.pop();
currentLine = [spacesOnLeft(elements.map(e => e.spaces))];
continue;
}

currentLine.push(t);

// Token can be a key, value or metadata, only key and value are valid for line count
// Metadata are tokens started with # like #uuid
if (!t.startsWith('#')) {
keyValue.push(t);
}

let endLine = false;

// If the line already contains a key and value, go to next line
if (keyValue.length === elements.at(-1)?.perLine) {
keyValue = [];
endLine = true;
}

// If line is not ending and next token is not a closing delimiter, continue for next token
if (!endLine && !nextEnding) {
currentLine.push(' ');
continue;
}

// If the next token is a close delimiter, use this delimiter instead of key/value for end of line
if (nextEnding) {
currentLine.push(nextToken);
tokenUsed = true;
elements.pop();
}

lines.push(currentLine);
currentLine = [spacesOnLeft(elements.map(e => e.spaces))];
}

lines.push(currentLine);

return lines.map(l => l.join('')).filter(e => e);
}

export const ednPrettify = (edn: string) => {
const tokens = tokenize(edn);

if (!startDelimiters.includes(tokens[0])) {
return edn;
}

const lines = tokensToLines(tokens);

return lines.join('\n');
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[:foo :bar]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[:foo
:bar]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{:foo :bar,
:bar :foo}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{:foo :bar
:bar :foo}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:foo :bar :foo-set #{} :arr []}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{:foo :bar
:foo-set #{}
:arr []}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:level1 {:level2 {:level3 {:foo :bar :foo2 :bar2} :bar :foo}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{:level1 {:level2 {:level3 {:foo :bar
:foo2 :bar2}
:bar :foo}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:foo #metadata :bar :another :bar}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{:foo #metadata :bar
:another :bar}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"Hello World!"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"Hello World!"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello World!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello World!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#{:foo :bar :john :doe}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#{:foo
:bar
:john
:doe}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:foo "some, text"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:foo "some, text"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:foo "some,\ntext"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:foo "some,\ntext"}
Loading