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
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ native `<input type=color>`, no Acrylic or Accent opacity slider. Spec and plan:
Deliberately no semantic diagnostics: without a tsconfig or node_modules they would be noise.
Every language loads on demand (one Vite chunk each). Prose (`.txt`, `.log`, `.csv`, subtitles)
gets no gutter and no language. Token colours are the THEME's since #298 (`--p-code-*`).
**JSONC COMMENTS ARE COMMENTS** (#312; owner, 2026-10-07: "comments in jsonc arent read as
comments in prism"): the strict JSON grammar made every // line plain text under a squiggle.
`.jsonc`, `tsconfig(.*).json`, `jsconfig(.*).json`, `.eslintrc(.json)`, `.babelrc(.json)`,
`(.)devcontainer.json`, `wrangler/turbo/biome/deno.json` and every `.json` in a `.vscode`
folder go to `lib/jsoncMode.ts` (a tokenizer with the JSON grammar's own tags, comment tags,
brace folding) and `codeLint.jsoncErrors` (comments and trailing commas blanked to spaces,
offsets kept, then `JSON.parse`), so a missing comma is still underlined. `.json5` colours the
same with no linter (bare keys and single quotes). Plain `.json` stays strict. `jsonc` e2e.
**A FILE THAT GROWS** (2026-08-31): "Follow the file" appends new bytes as they are
written - a build log, an agent's transcript - and a file PAST THE 64MB CEILING now shows
its TAIL (2MB) instead of an apology. Both are READ-ONLY, and structurally so: a followed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "prism",
"version": "0.94.1",
"version": "0.94.2",
"description": "A fast, universal media viewer for Windows.",
"author": "Max",
"license": "MIT",
Expand Down
15 changes: 5 additions & 10 deletions src/renderer/src/components/CodeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { ContextMenu, type MenuItem } from './ContextMenu'
import { fileVerbs, tickIf } from '../lib/fileVerbs'
import { lintKeymap } from '@codemirror/lint'
import { isProse, langFor } from '../lib/codeLang'
import { jsonLinter, syntaxLinter } from '../lib/codeLint'
import { lintFor } from '../lib/codeLint'
import { prismCodeTheme } from '../lib/codeTheme'
import { setWrapPref, useWrapPref, wrapPref, wrapsFor, type WrapPref } from '../lib/codePrefs'
import { reloadAction, stampChanged, touchesFile, type Stamp } from '../lib/fileReload'
Expand Down Expand Up @@ -257,7 +257,7 @@ export function CodeView({
// match it. Runs on every path change, since the view outlives the file.
useEffect(() => {
let alive = true
const lang = langFor(name)
const lang = langFor(name, path)
void Promise.all([
window.prism.readText(path),
lang ? lang.load() : Promise.resolve<Extension>([]),
Expand Down Expand Up @@ -290,14 +290,9 @@ export function CodeView({
wrapComp.reconfigure(wrapFor(name, wrapPref())),
langComp.reconfigure(langExt),
// Squiggles need a grammar to be wrong against. A stream lexer has
// none, so those languages get colour and no claims about errors.
lintComp.reconfigure(
/\.jsonc?$|\.json5$/i.test(name)
? [syntaxLinter, jsonLinter]
: lang?.parsed
? syntaxLinter
: []
)
// none, so those languages get colour and no claims about errors;
// JSONC is the one lexed language with a checker of its own (#312).
lintComp.reconfigure(lintFor(lang))
]
})
report(body === disk ? null : body)
Expand Down
49 changes: 44 additions & 5 deletions src/renderer/src/lib/codeLang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type CodeLang = {
readonly name: string
/** True when the grammar can report syntax errors. */
readonly parsed: boolean
/** A checker beyond the grammar's error nodes: JSON.parse, or its JSONC form. */
readonly lint?: 'json' | 'jsonc'
readonly load: () => Promise<Extension>
}

Expand All @@ -40,7 +42,21 @@ const JS = parsed('JavaScript', async () => (await import('@codemirror/lang-java
const JSX = parsed('JSX', async () => (await import('@codemirror/lang-javascript')).javascript({ jsx: true }))
const TS = parsed('TypeScript', async () => (await import('@codemirror/lang-javascript')).javascript({ typescript: true }))
const TSX = parsed('TSX', async () => (await import('@codemirror/lang-javascript')).javascript({ jsx: true, typescript: true }))
const JSON_ = parsed('JSON', async () => (await import('@codemirror/lang-json')).json())
const JSON_: CodeLang = {
...parsed('JSON', async () => (await import('@codemirror/lang-json')).json()),
lint: 'json'
}
// JSON with comments (#312): the strict grammar read every // line as an error.
// A tokenizer colours it and codeLint's jsoncErrors judges it.
const JSONC: CodeLang = {
name: 'JSON with Comments',
parsed: false,
lint: 'jsonc',
load: async () => (await import('./jsoncMode')).jsonc()
}
// JSON5 has bare keys, single quotes and hex, which jsoncErrors would call
// wrong: the same colours, and no claims about errors.
const JSON5: CodeLang = { name: 'JSON5', parsed: false, load: JSONC.load }
const CSS = parsed('CSS', async () => (await import('@codemirror/lang-css')).css())
const HTML = parsed('HTML', async () => (await import('@codemirror/lang-html')).html())
const XML = parsed('XML', async () => (await import('@codemirror/lang-xml')).xml())
Expand Down Expand Up @@ -121,7 +137,7 @@ const COBOL = lexed('COBOL', async () => (await import('@codemirror/legacy-modes
const BY_EXT: Record<string, CodeLang> = {
'.js': JS, '.mjs': JS, '.cjs': JS, '.jsx': JSX,
'.ts': TS, '.mts': TS, '.cts': TS, '.tsx': TSX,
'.json': JSON_, '.jsonc': JSON_, '.json5': JSON_, '.ipynb': JSON_,
'.json': JSON_, '.jsonc': JSONC, '.json5': JSON5, '.ipynb': JSON_,
'.css': CSS, '.scss': SCSS, '.sass': SASS, '.less': LESS, '.styl': STYLUS,
'.html': HTML, '.xhtml': HTML, '.svelte': HTML, '.astro': HTML, '.vue': VUE,
'.xml': XML, '.svg': XML, '.svgz': XML,
Expand Down Expand Up @@ -189,19 +205,42 @@ const BY_NAME: Record<string, CodeLang> = {
'.editorconfig': PROPERTIES,
'.env': PROPERTIES,
'.prettierrc': JSON_,
'.eslintrc': JSON_,
'.babelrc': JSON_
// The tools that read these allow comments in them, so JSONC (#312).
'.eslintrc': JSONC,
'.eslintrc.json': JSONC,
'.babelrc': JSONC,
'.babelrc.json': JSONC,
'tsconfig.json': JSONC,
'jsconfig.json': JSONC,
'devcontainer.json': JSONC,
'.devcontainer.json': JSONC,
'wrangler.json': JSONC,
'turbo.json': JSONC,
'biome.json': JSONC,
'deno.json': JSONC
}

/** `tsconfig.app.json` and the like: the variants read the same way. */
const JSONC_FAMILY = /^(?:tsconfig|jsconfig)\..+\.json$/

/** The folder a path sits in, lower-cased, or '' without one. */
function parentName(path: string): string {
const parts = path.split(/[\\/]/)
return (parts[parts.length - 2] ?? '').toLowerCase()
}

/**
* The language for a file, or null when Prism has nothing to colour it with.
* Null is a real answer, not a failure: prose (`.txt`, `.log`) is meant to be
* plain, and an unmapped extension reads better uncoloured than mis-coloured.
*/
export function langFor(name: string): CodeLang | null {
export function langFor(name: string, path?: string): CodeLang | null {
const n = name.toLowerCase()
const byName = BY_NAME[n]
if (byName) return byName
if (JSONC_FAMILY.test(n)) return JSONC
// VS Code's own folder: settings, launch, tasks, extensions all take comments.
if (path && n.endsWith('.json') && parentName(path) === '.vscode') return JSONC
const ext = /\.[^.]*$/.exec(n)?.[0] ?? ''
return BY_EXT[ext] ?? null
}
Expand Down
88 changes: 87 additions & 1 deletion src/renderer/src/lib/codeLint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language'
import { linter, type Diagnostic } from '@codemirror/lint'
import type { EditorState } from '@codemirror/state'
import type { EditorState, Extension } from '@codemirror/state'
import type { CodeLang } from './codeLang'

// The red underline. Prism does not run a language server, so this is honest
// about what it knows: where a Lezer grammar failed to parse, and nothing more.
Expand Down Expand Up @@ -88,8 +89,93 @@ export function jsonErrors(text: string): Diagnostic[] {
}
}

/**
* JSONC as plain JSON with the same offsets (#312): every comment and every
* trailing comma becomes spaces (line breaks kept), so `JSON.parse` judges
* what is left and a position it names is a position in the file. Strings are
* copied untouched, so a "//" inside a url stays. Exported for tests.
*/
export function blankJsonc(text: string): { json: string; unclosedComment: number | null } {
const out = text.split('')
const blank = (from: number, to: number): void => {
for (let i = from; i < to; i++) if (out[i] !== '\n' && out[i] !== '\r') out[i] = ' '
}
// The last comma seen, until something other than space shows whether a
// closer follows it (comments are already gone by then).
let comma = -1
let i = 0
while (i < text.length) {
const c = text[i]
if (c === '"') {
comma = -1
let j = i + 1
while (j < text.length && text[j] !== '"' && text[j] !== '\n') j += text[j] === '\\' ? 2 : 1
i = j + 1
continue
}
if (c === '/' && text[i + 1] === '/') {
let j = i
while (j < text.length && text[j] !== '\n') j++
blank(i, j)
i = j
continue
}
if (c === '/' && text[i + 1] === '*') {
const end = text.indexOf('*/', i + 2)
if (end < 0) {
blank(i, text.length)
return { json: out.join(''), unclosedComment: i }
}
blank(i, end + 2)
i = end + 2
continue
}
if (c === ',') comma = i
else if (c === '}' || c === ']') {
if (comma >= 0) out[comma] = ' '
comma = -1
} else if (!/\s/.test(c)) comma = -1
i++
}
return { json: out.join(''), unclosedComment: null }
}

/**
* JSON's rules less the two JSONC relaxes: comments and trailing commas are
* fine, a missing comma or an unclosed brace is still an error, said by
* `JSON.parse` at its real place in the file. Exported for tests.
*/
export function jsoncErrors(text: string): Diagnostic[] {
const { json, unclosedComment } = blankJsonc(text)
if (unclosedComment !== null) {
return [
{
from: unclosedComment,
to: Math.min(unclosedComment + 2, text.length),
severity: 'error' as const,
message: 'This comment is never closed'
}
]
}
return jsonErrors(json)
}

/**
* Which squiggles a file gets. A grammar's own error nodes for a parsed
* language, plus `JSON.parse` for JSON; JSONC gets its own check and NEVER the
* grammar's (it has none that knows comments); a stream lexer gets nothing.
*/
export function lintFor(lang: CodeLang | null): Extension {
if (lang?.lint === 'jsonc') return jsoncLinter
if (lang?.lint === 'json') return [syntaxLinter, jsonLinter]
return lang?.parsed ? syntaxLinter : []
}

/** Squiggles from the grammar. Only worth attaching to a parsed language. */
export const syntaxLinter = linter((view) => parseErrors(view.state), { delay: 300 })

/** Squiggles from JSON.parse, which explains itself better than the grammar can. */
export const jsonLinter = linter((view) => jsonErrors(view.state.doc.toString()), { delay: 300 })

/** Squiggles for JSON with comments: JSON's errors, never its comments. */
export const jsoncLinter = linter((view) => jsoncErrors(view.state.doc.toString()), { delay: 300 })
Loading
Loading