diff --git a/apps/svelte.dev/src/lib/tutorial/adapters/rollup/index.svelte.ts b/apps/svelte.dev/src/lib/tutorial/adapters/rollup/index.svelte.ts index 7daf5dccd5..a527605c67 100644 --- a/apps/svelte.dev/src/lib/tutorial/adapters/rollup/index.svelte.ts +++ b/apps/svelte.dev/src/lib/tutorial/adapters/rollup/index.svelte.ts @@ -1,8 +1,9 @@ -import type { Adapter, Warning } from '$lib/tutorial'; -import type { File, Item } from 'editor'; import Bundler from '@sveltejs/repl/bundler'; // @ts-ignore package exports don't have types import * as yootils from 'yootils'; +import type { Adapter } from '$lib/tutorial'; +import type { File, Item } from 'editor'; +import type { CompileError, Warning } from 'svelte/compiler'; /** Rollup bundler singleton */ let bundler: Bundler; @@ -10,6 +11,7 @@ let bundler: Bundler; export const state = new (class RollupState { progress = $state.raw({ value: 0, text: 'initialising' }); bundle = $state.raw(null); + errors = $state.raw>(); warnings = $state.raw>({}); })(); @@ -51,15 +53,23 @@ export async function create(): Promise { type: f.name.split('.').pop() ?? 'svelte' })) ); + state.bundle = result; - const _warnings: Record = {}; + // TODO this approach is insufficient — we need to get diagnostics for + // individual files, not just the bundle as a whole + state.errors = {}; + state.warnings = {}; + + if (result.error) { + const file = '/src/lib/' + result.error.filename; + state.errors[file] = result.error; + } + for (const warning of result?.warnings ?? []) { const file = '/src/lib/' + warning.filename; - _warnings[file] = _warnings[file] || []; - _warnings[file].push(warning); + (state.warnings[file] ??= []).push(warning); } - state.warnings = _warnings; } const q = yootils.queue(1); diff --git a/apps/svelte.dev/src/lib/tutorial/adapters/webcontainer/index.svelte.ts b/apps/svelte.dev/src/lib/tutorial/adapters/webcontainer/index.svelte.ts index 31f0b712dc..29e13709e1 100644 --- a/apps/svelte.dev/src/lib/tutorial/adapters/webcontainer/index.svelte.ts +++ b/apps/svelte.dev/src/lib/tutorial/adapters/webcontainer/index.svelte.ts @@ -6,8 +6,9 @@ import * as yootils from 'yootils'; import { get_depth } from '../../../utils/path.js'; import { escape_html } from '../../../utils/escape.js'; import { ready } from '../common/index.js'; -import type { Adapter, Warning } from '$lib/tutorial'; +import type { Adapter } from '$lib/tutorial'; import type { Item, File } from 'editor'; +import type { CompileError, Warning } from 'svelte/compiler'; const converter = new AnsiToHtml({ fg: 'var(--sk-text-3)' @@ -21,6 +22,7 @@ export const state = new (class WCState { base = $state.raw(null); error = $state.raw(null); logs = $state.raw([]); + errors = $state.raw>({}); warnings = $state.raw>({}); })(); @@ -47,7 +49,7 @@ export async function create(): Promise { } }); - let warnings: Record = {}; + let warnings: Record = {}; let timeout: any; function schedule_to_update_warning(msec: number) { @@ -63,13 +65,20 @@ export async function create(): Promise { state.logs = []; } else if (chunk?.startsWith('svelte:warnings:')) { const warn: Warning = JSON.parse(chunk.slice(16)); - const filename = warn.filename.startsWith('/') ? warn.filename : '/' + warn.filename; + const filename = (warn.filename!.startsWith('/') ? warn.filename : '/' + warn.filename)!; const current = warnings[filename]; if (!current) { warnings[filename] = [warn]; // the exact same warning may be given multiple times in a row - } else if (!current.some((s) => s.code === warn.code && s.pos === warn.pos)) { + } else if ( + !current.some( + (s) => + s.code === warn.code && + s.position![0] === warn.position![0] && + s.position![1] === warn.position![1] + ) + ) { current.push(warn); } diff --git a/apps/svelte.dev/src/lib/tutorial/index.d.ts b/apps/svelte.dev/src/lib/tutorial/index.d.ts index 900356b80b..efc4f9a43c 100644 --- a/apps/svelte.dev/src/lib/tutorial/index.d.ts +++ b/apps/svelte.dev/src/lib/tutorial/index.d.ts @@ -64,17 +64,6 @@ export interface EditingConstraints { remove: Set; } -// TODO replace with `Warning` from `svelte/compiler` -export interface Warning { - code: string; - start: { line: number; column: number; character: number }; - end: { line: number; column: number; character: number }; - pos: number; - filename: string; - frame: string; - message: string; -} - export interface MenuItem { icon: string; label: string; diff --git a/apps/svelte.dev/src/routes/tutorial/[...slug]/+page.svelte b/apps/svelte.dev/src/routes/tutorial/[...slug]/+page.svelte index 6c0c9523d3..a1c9d71b95 100644 --- a/apps/svelte.dev/src/routes/tutorial/[...slug]/+page.svelte +++ b/apps/svelte.dev/src/routes/tutorial/[...slug]/+page.svelte @@ -316,6 +316,7 @@
{ diff --git a/apps/svelte.dev/src/routes/tutorial/[...slug]/adapter.svelte.ts b/apps/svelte.dev/src/routes/tutorial/[...slug]/adapter.svelte.ts index 86982e2af9..ab2d84ad77 100644 --- a/apps/svelte.dev/src/routes/tutorial/[...slug]/adapter.svelte.ts +++ b/apps/svelte.dev/src/routes/tutorial/[...slug]/adapter.svelte.ts @@ -31,7 +31,9 @@ export const adapter_state = new (class { status: 'initialising' } ); - /** Compiler warnings */ + + /** Diagnostics */ + errors = $derived((use_rollup ? rollup_state.errors : wc_state.errors) || {}); warnings = $derived((use_rollup ? rollup_state.warnings : wc_state.warnings) || {}); })(); diff --git a/packages/editor/src/lib/Editor.svelte b/packages/editor/src/lib/Editor.svelte index a162e957ef..f955e03365 100644 --- a/packages/editor/src/lib/Editor.svelte +++ b/packages/editor/src/lib/Editor.svelte @@ -14,17 +14,18 @@ import { autocomplete_for_svelte } from '@sveltejs/site-kit/codemirror'; import type { Diagnostic } from '@codemirror/lint'; import { Workspace, type Item, type File } from './Workspace.svelte.js'; - import type { Warning } from 'svelte/compiler'; + import type { CompileError, Warning } from 'svelte/compiler'; import './codemirror.css'; interface Props { - warnings: Record; // TODO this should include errors as well + errors: Record; + warnings: Record; workspace: Workspace; onchange: (file: File, contents: string) => void; autocomplete_filter?: (file: File) => boolean; } - let { warnings, workspace, onchange, autocomplete_filter = () => true }: Props = $props(); + let { errors, warnings, workspace, onchange, autocomplete_filter = () => true }: Props = $props(); let container: HTMLDivElement; @@ -166,25 +167,51 @@ }); $effect(() => { - if (editor_view) { - if (workspace.selected_name) { - const current_warnings = warnings[workspace.selected_name] || []; - const diagnostics = current_warnings.map((warning) => { - /** @type {import('@codemirror/lint').Diagnostic} */ - const diagnostic: Diagnostic = { - from: warning.start!.character, - to: warning.end!.character, - severity: 'warning', - message: warning.message - }; - - return diagnostic; - }); + if (!editor_view || !workspace.selected_name) return; + + const diagnostics: Diagnostic[] = []; + + const error = null; // TODO should be `errors[workspace.selected_name]` but it's currently a Rollup plugin error... + const current_warnings = warnings[workspace.selected_name] || []; + + if (error) { + // diagnostics.push({ + // severity: 'error', + // from: error.position![0], + // to: error.position![1], + // message: error.message, + // renderMessage: () => { + // // TODO expose error codes, so we can link to docs in future + // const span = document.createElement('span'); + // span.innerHTML = `${error.message + // .replace(/&/g, '&') + // .replace(/$1`)}`; + // return span; + // } + // }); + } - const transaction = setDiagnostics(editor_view.state, diagnostics); - editor_view.dispatch(transaction); - } + for (const warning of current_warnings) { + diagnostics.push({ + severity: 'warning', + from: warning.start!.character, + to: warning.end!.character, + message: warning.message, + renderMessage: () => { + const span = document.createElement('span'); + span.innerHTML = `${warning.message + .replace(/&/g, '&') + .replace(/$1`)} (${warning.code})`; + + return span; + } + }); } + + const transaction = setDiagnostics(editor_view.state, diagnostics); + editor_view.dispatch(transaction); }); diff --git a/packages/editor/src/lib/codemirror.css b/packages/editor/src/lib/codemirror.css index d9932c1d01..f47639ec1a 100644 --- a/packages/editor/src/lib/codemirror.css +++ b/packages/editor/src/lib/codemirror.css @@ -3,122 +3,240 @@ } .cm-editor { + color: var(--sk-code-base); + background-color: transparent; height: 100%; -} - -.cm-editor .cm-scroller { - font: var(--sk-font-mono); -} - -.cm-editor .cm-gutters { - background: var(--sk-back-3); - border-right: none; - padding: 0; - width: 5rem; -} - -.cm-editor .cm-activeLine { - background: var(--sk-back-translucent); -} - -.cm-editor .cm-activeLineGutter { - background-color: var(--sk-back-3); -} - -.cm-editor .cm-lineNumbers { - flex: 1; - color: #237893; -} - -.cm-editor .cm-foldGutter { - width: 1rem; -} - -.cm-focused .cm-cursor { - border-left-color: var(--sk-text-3); -} - -.cm-editor .cm-content { - padding: 1rem 0; -} - -.cm-editor .cm-line { - padding: 0 1rem; -} - -.cm-editor .cm-selectionBackground { - border-radius: 2px; -} - -.cm-editor .cm-selectionMatch { - background: var(--selection-color); - color: var(--sk-text-2); -} - -.cm-editor .cm-tooltip { - background: var(--sk-back-2); - color: var(--sk-text-1); - border: none; - border-radius: 2px; - overflow: hidden; - margin: 0.4rem 0; - filter: drop-shadow(1px 2px 5px rgba(0, 0, 0, 0.1)); -} - -.cm-editor .cm-tooltip-hover { - border: none; -} - -.cm-editor .cm-tooltip-below { -} -.cm-editor .cm-tooltip-lint { -} -.cm-editor .cm-tooltip-section { -} -.cm-editor .cm-diagnostic { - border: none; - padding: 0.2rem 0.8rem; -} - -.cm-editor .cm-diagnostic-warning { - background: var(--sk-theme-1-variant); - color: white; -} - -.cm-editor .cm-diagnosticText { -} - -.cm-editor .cm-tooltip.cm-tooltip-autocomplete > ul { - font: var(--sk-font-mono); -} - -.cm-editor .cm-panels { - background: var(--sk-back-4); - color: var(--sk-text-1); -} - -.cm-editor .cm-panels.cm-panels-top, -.cm-editor .cm-panels.cm-panels-bottom { - border-top: 1px solid var(--sk-back-5); - border-bottom: 1px solid var(--sk-back-5); -} - -.cm-editor .cm-button { - background: var(--sk-back-5); - border: 2px solid transparent; -} - -.cm-editor .cm-textfield { - background: var(--sk-back-1); - color: var(--sk-text-1); - border: 2px solid transparent; -} - -.cm-editor .cm-search button:focus-visible, -.cm-editor .cm-search input:focus-visible { - border: 2px solid var(--flash); -} -.cm-editor .cm-search input[type='checkbox']:focus-visible { - outline: 2px solid var(--flash); + &.cm-focused { + .cm-cursor { + border-left-color: var(--sk-text-3); + } + + > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, + .cm-selectionBackground, + .cm-content ::selection { + background-color: var(--sk-selection-color); + } + + .cm-matchingBracket, + .cm-nonmatchingBracket { + background-color: #bad0f847; + } + } + + .cm-scroller { + font: var(--sk-font-mono); + } + + .cm-gutters { + background: var(--sk-back-3); + border: none; + padding: 0; + width: 5rem; + } + + .cm-activeLine { + background: var(--sk-back-translucent); + } + + .cm-activeLineGutter { + background-color: var(--sk-back-3); + } + + .cm-lineNumbers { + flex: 1; + + .cm-gutterElement:not(:last-child) { + display: flex; + justify-content: end; + align-items: end; + } + } + + .cm-foldGutter { + width: 1rem; + } + + .cm-foldPlaceholder { + background-color: transparent; + border: none; + color: #ddd; + } + + .cm-lintRange { + background-position: left bottom; + background-repeat: repeat-x; + padding-bottom: 4px; + + &.cm-lintRange-error { + /* TODO */ + } + + &.cm-lintRange-warning { + /* TODO */ + } + } + + .cm-content { + /* ensure no gap between top of editor and highlighted first/last line */ + padding-top: 0; + padding-bottom: 0; + + .cm-line:first-child { + padding-top: 4px; + } + + .cm-line:last-child { + padding-bottom: 4px; + } + } + + .cm-line { + padding: 0 1rem; + } + + .cm-selectionBackground { + border-radius: 2px; + } + + .cm-selectionMatch { + background: var(--selection-color); + color: var(--sk-text-2); + } + + .cm-tooltip.cm-tooltip-autocomplete { + color: var(--sk-text-2) !important; + perspective: 1px; + + & > ul > li[aria-selected] { + background-color: var(--sk-back-4); + color: var(--sk-text-1) !important; + } + + & > ul { + font: var(--sk-font-mono); + } + } + + .cm-panels { + background: var(--sk-back-4); + color: var(--sk-text-1); + + &.cm-panels-top, + &.cm-panels-bottom { + border-top: 1px solid var(--sk-back-5); + border-bottom: 1px solid var(--sk-back-5); + } + } + + .cm-button { + background: var(--sk-back-5); + border: 2px solid transparent; + } + + .cm-textfield { + background: var(--sk-back-1); + color: var(--sk-text-1); + border: 2px solid transparent; + } + + .cm-search button:focus-visible, + .cm-search input:focus-visible { + border: 2px solid var(--flash); + } + + .cm-search input[type='checkbox']:focus-visible { + outline: 2px solid var(--flash); + } + + .cm-searchMatch.cm-searchMatch-selected { + background-color: #6199ff2f; + } + + .cm-tooltip { + --warning: hsl(40 100% 70%); + --error: hsl(0 100% 90%); + border: none; + background: var(--sk-back-3); + font: var(--sk-font-ui-small); + max-width: calc(100vw - 10em); + position: relative; + padding: 1rem; + filter: var(--sk-shadow); + + :root.dark { + --warning: hsl(40 100% 50%); + --error: hsl(0 100% 70%); + } + + &:has(.cm-diagnostic) { + background: transparent; + } + + &:has(.cm-diagnostic-warning) { + --bg: var(--warning); + --fg: #222; + } + + &:has(.cm-diagnostic-error) { + --bg: var(--error); + --fg: #222; + } + + .cm-tooltip-section { + position: relative; + /* left: -1rem; */ + padding: 1rem; + background: var(--bg); + border-radius: 2px; + max-width: 64em; + + .cm-diagnostic { + padding: 0; + margin: 0; + position: relative; + border: none; + border-radius: var(--sk-border-radius); + + &:not(:last-child) { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + margin-bottom: 1rem; + padding-bottom: 1rem; + } + + :not(code) { + font: var(--sk-font-ui-small); + } + + .cm-diagnosticText { + position: relative; + display: flex; + color: var(--fg); + z-index: 2; + + code { + color: inherit; + background-color: rgba(0, 0, 0, 0.05); + font-size: 1em; + margin: 0; /* TODO this counteracts base styles that should probably be deleted? */ + padding: 0.2rem 0.4rem; + } + + strong { + font: var(--sk-font-mono); + font-size: 1em; + opacity: 0.7; + } + } + } + } + + &.cm-tooltip-below .cm-tooltip-section { + /* top: 1rem; */ + } + + &.cm-tooltip-above .cm-tooltip-section { + /* bottom: 1rem; */ + } + } } diff --git a/packages/repl/src/lib/theme.ts b/packages/repl/src/lib/theme.ts index 6bf7b0c973..86e9a6f06b 100644 --- a/packages/repl/src/lib/theme.ts +++ b/packages/repl/src/lib/theme.ts @@ -1,122 +1,7 @@ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; -import { EditorView } from '@codemirror/view'; import { tags as t } from '@lezer/highlight'; -const ERROR_HUE = 0; -const WARNING_HUE = 40; - -const WARNING_FG = `hsl(${WARNING_HUE} 100% 60%)`; -const WARNING_BG = `hsl(${WARNING_HUE} 100% 40% / 0.5)`; - -const ERROR_FG = `hsl(${ERROR_HUE} 100% 40%)`; -const ERROR_BG = `hsl(${ERROR_HUE} 100% 40% / 0.5)`; - -function svg(content: string, attrs = `viewBox="0 0 40 40"`) { - return `url('data:image/svg+xml,${encodeURIComponent( - content - )}')`; -} - -function underline(color: string) { - return svg( - ``, - `width="6" height="4"` - ); -} - -const svelteThemeStyles = EditorView.theme( - { - '&': { - color: 'var(--sk-code-base)', - backgroundColor: 'transparent' - }, - - '.cm-content': { - // ensure no gap between top of editor and highlighted first/last line - paddingTop: '0px', - paddingBottom: '0px' - }, - - '.cm-content .cm-line:first-child': { - paddingTop: '4px' - }, - - '.cm-content .cm-line:last-child': { - paddingBottom: '4px' - }, - - '.cm-lineNumbers .cm-gutterElement:not(:last-child)': { - display: 'flex', - justifyContent: 'end', - alignItems: 'end' - }, - - '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': - { backgroundColor: 'var(--sk-selection-color)' }, - - '.cm-panels': { backgroundColor: 'var(--sk-back-2)', color: 'var(--sk-text-2)' }, - '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' }, - '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' }, - - '.cm-searchMatch.cm-searchMatch-selected': { - backgroundColor: '#6199ff2f' - }, - - '.cm-activeLine': { backgroundColor: '#6699ff0b' }, - '.cm-selectionMatch': { backgroundColor: '#aafe661a' }, - - '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { - backgroundColor: '#bad0f847' - }, - - '.cm-gutters': { - backgroundColor: 'var(--sk-back-3)', - border: 'none' - }, - - '.cm-activeLineGutter': { - backgroundColor: 'var(--sk-back-4)' - }, - - '.cm-foldPlaceholder': { - backgroundColor: 'transparent', - border: 'none', - color: '#ddd' - }, - - // https://github.com/codemirror/lint/blob/271b35f5d31a7e3645eaccbfec608474022098e1/src/lint.ts#L620 - '.cm-lintRange': { - backgroundPosition: 'left bottom', - backgroundRepeat: 'repeat-x', - paddingBottom: '4px' - }, - '.cm-lintRange-error': { - backgroundImage: underline(ERROR_FG) - }, - '.cm-lintRange-warning': { - backgroundImage: underline(WARNING_FG) - }, - '.cm-tooltip .cm-tooltip-arrow:before': { - borderTopColor: 'transparent', - borderBottomColor: 'transparent' - }, - '.cm-tooltip .cm-tooltip-arrow:after': { - borderTopColor: 'var(--sk-back-3)', - borderBottomColor: 'var(--sk-back-3)' - }, - '.cm-tooltip-autocomplete': { - color: 'var(--sk-text-2) !important', - perspective: '1px', - '& > ul > li[aria-selected]': { - backgroundColor: 'var(--sk-back-4)', - color: 'var(--sk-text-1) !important' - } - } - }, - { dark: true } -); - -/// The highlighting style for code in the One Dark theme. +// TODO move this into the `editor` package const svelteHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: 'var(--sk-code-keyword)' }, { @@ -154,4 +39,4 @@ const svelteHighlightStyle = HighlightStyle.define([ { tag: t.invalid, color: '#ff008c' } ]); -export const svelteTheme = [svelteThemeStyles, syntaxHighlighting(svelteHighlightStyle)]; +export const svelteTheme = syntaxHighlighting(svelteHighlightStyle); diff --git a/packages/repl/src/lib/workers/bundler/index.ts b/packages/repl/src/lib/workers/bundler/index.ts index b21afe2599..f670797f78 100644 --- a/packages/repl/src/lib/workers/bundler/index.ts +++ b/packages/repl/src/lib/workers/bundler/index.ts @@ -14,7 +14,7 @@ import loop_protect from './plugins/loop-protect'; import type { Plugin, TransformResult } from '@rollup/browser'; import type { BundleMessageData } from '../workers'; import type { File, Warning } from '../../types'; -import type { CompileResult } from 'svelte/compiler'; +import type { CompileError, CompileResult } from 'svelte/compiler'; let packages_url: string; let svelte_url: string; @@ -502,7 +502,6 @@ async function bundle({ uid, files }: { uid: number; files: File[] }) { cached.client, lookup ); - let error; try { if (client.error) { @@ -553,11 +552,7 @@ async function bundle({ uid, files }: { uid: number; files: File[] }) { } catch (err) { console.error(err); - // @ts-ignore - const e: Error = error || err; - - // @ts-ignore - delete e.toString; + const e = err as CompileError; return { uid, @@ -565,10 +560,7 @@ async function bundle({ uid, files }: { uid: number; files: File[] }) { server: null, imports: null, warnings: client.warnings, - error: Object.assign({}, e, { - message: e.message, - stack: e.stack - }) + error: { ...e } }; } }