Skip to content
10 changes: 10 additions & 0 deletions src-web/components/core/Editor/Editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@
@apply pl-[1px];
}

/* Comment styling */
.cm-comment,
.cm-comment * {
color: #6b7280 !important;
}

.cm-comment {
@apply opacity-70;
}

.cm-placeholder {
@apply text-placeholder;
}
Expand Down
9 changes: 6 additions & 3 deletions src-web/components/core/Editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { startCompletion } from '@codemirror/autocomplete';
import { defaultKeymap, historyField, indentWithTab } from '@codemirror/commands';
import { defaultKeymap, historyField, indentWithTab, toggleComment } from '@codemirror/commands';
import { foldState, forceParsing } from '@codemirror/language';
import type { EditorStateConfig, Extension } from '@codemirror/state';
import { Compartment, EditorState } from '@codemirror/state';
Expand Down Expand Up @@ -40,6 +40,7 @@ import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
import { IconButton } from '../IconButton';
import { HStack } from '../Stacks';
import './Editor.css';
import { commentHighlightPlugin } from './commentHighlight';
import {
baseExtensions,
getLanguageExtension,
Expand All @@ -55,8 +56,8 @@ const vsCodeWithoutTab = vscodeKeymap.filter((k) => k.key !== 'Tab');
const keymapExtensions: Record<EditorKeymap, Extension> = {
vim: vim(),
emacs: emacs(),
vscode: keymap.of(vsCodeWithoutTab),
default: [],
vscode: keymap.of([...vsCodeWithoutTab, { key: 'Mod-/', run: toggleComment }]),
default: keymap.of([{ key: 'Mod-/', run: toggleComment }]),
};

export interface EditorProps {
Expand Down Expand Up @@ -389,6 +390,8 @@ function EditorInner({
keymapCompartment.current.of(
keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default,
),
EditorState.languageData.of(() => [{ commentTokens: { line: '//' } }]),
commentHighlightPlugin(),
...getExtensions({
container,
readOnly,
Expand Down
70 changes: 70 additions & 0 deletions src-web/components/core/Editor/commentHighlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, type EditorView, ViewPlugin } from '@codemirror/view';

const commentMark = Decoration.mark({
class: 'cm-comment',
attributes: { 'data-comment': 'true' },
});

function getCommentDecorations(view: EditorView): DecorationSet {
const decorations: Range<Decoration>[] = [];
const doc = view.state.doc.toString();

// Handle single-line comments
for (let i = 1; i <= view.state.doc.lines; i++) {
const line = view.state.doc.line(i);
const text = line.text;

const commentIndex = text.indexOf('//');

if (commentIndex !== -1) {
// Check if it's at the start or after some code
const beforeComment = text.substring(0, commentIndex).trim();

// Highlight if: line starts with // OR there's code before it (like after a comma)
if (
beforeComment === '' ||
beforeComment.endsWith(',') ||
beforeComment.endsWith('{') ||
beforeComment.endsWith('[')
) {
const commentStart = line.from + commentIndex;
decorations.push(commentMark.range(commentStart, line.to));
}
}
}

// Handle multi-line comments
const multiLineRegex = /\/\*[\s\S]*?\*\//g;
let match: RegExpExecArray | null = null;
// biome-ignore lint/suspicious/noAssignInExpressions: Standard pattern for regex matching
while ((match = multiLineRegex.exec(doc)) !== null) {
decorations.push(commentMark.range(match.index, match.index + match[0].length));
}

return Decoration.set(decorations, true);
}

export function commentHighlightPlugin() {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;

constructor(view: EditorView) {
this.decorations = getCommentDecorations(view);
}

update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = getCommentDecorations(update.view);
}
}
},
{
decorations(v) {
return v.decorations;
},
},
);
}
13 changes: 7 additions & 6 deletions src-web/components/core/Editor/json-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ import type { EditorView } from '@codemirror/view';
import { parse as jsonLintParse } from '@prantlf/jsonlint';

const TEMPLATE_SYNTAX_REGEX = /\$\{\[[\s\S]*?]}/g;
const COMMENT_REGEX = /\/\/.*|\/\*[\s\S]*?\*\//g;

export function jsonParseLinter() {
return (view: EditorView): Diagnostic[] => {
try {
const doc = view.state.doc.toString();
// We need lint to not break on stuff like {"foo:" ${[ ... ]}} so we'll replace all template
// syntax with repeating `1` characters, so it's valid JSON and the position is still correct.
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => '1'.repeat(m.length));

const escapedDoc = doc
.replace(TEMPLATE_SYNTAX_REGEX, (m) => '1'.repeat(m.length))
.replace(COMMENT_REGEX, (m) => ' '.repeat(m.length));

jsonLintParse(escapedDoc);
// biome-ignore lint/suspicious/noExplicitAny: none
} catch (err: any) {
if (!('location' in err)) {
return [];
}

// const line = location?.start?.line;
// const column = location?.start?.column;
if (err.location.start.offset) {
if (err.location?.start?.offset !== undefined) {
return [
{
from: err.location.start.offset,
Expand Down
31 changes: 26 additions & 5 deletions src-web/hooks/useSendAnyHttpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,36 @@ import { getActiveCookieJar } from './useActiveCookieJar';
import { getActiveEnvironment } from './useActiveEnvironment';
import { createFastMutation, useFastMutation } from './useFastMutation';

// Helper function to strip both single-line (//) and multi-line (/* * /) comments.
const stripJsonComments = (v: string) => {
return v.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, '');
};

export function useSendAnyHttpRequest() {
return useFastMutation<HttpResponse | null, string, string | null>({
mutationKey: ['send_any_request'],
mutationFn: async (id) => {
const request = getModel('http_request', id ?? 'n/a');
if (request == null) {
return null;
if (request == null) return null;

const requestToSend = JSON.parse(JSON.stringify(request));

if (typeof requestToSend.body?.text === 'string') {
const text = requestToSend.body.text;
// Check if the text contains a '{' anywhere, or if it's just a string we want to allow comments in regardless.
if (text.includes('{') || text.trim().startsWith('//')) {
requestToSend.body.text = stripJsonComments(text);
}
}

return invokeCmd('cmd_send_http_request', {
request,
request: requestToSend,
environmentId: getActiveEnvironment()?.id,
cookieJarId: getActiveCookieJar()?.id,
});
},
});
}

export const sendAnyHttpRequest = createFastMutation<HttpResponse | null, string, string | null>({
mutationKey: ['send_any_request'],
mutationFn: async (id) => {
Expand All @@ -31,8 +43,17 @@ export const sendAnyHttpRequest = createFastMutation<HttpResponse | null, string
return null;
}

const requestToSend = JSON.parse(JSON.stringify(request));

if (typeof requestToSend.body?.text === 'string') {
const trimmed = requestToSend.body.text.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
requestToSend.body.text = stripJsonComments(requestToSend.body.text);
}
}

return invokeCmd('cmd_send_http_request', {
request,
request: requestToSend,
environmentId: getActiveEnvironment()?.id,
cookieJarId: getActiveCookieJar()?.id,
});
Expand Down