From efacf6c5434221f3b83132e624fd5c650bbf9a0b Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Thu, 19 Sep 2024 18:29:59 -0400 Subject: [PATCH] [Playground] Debug links (#1179) Co-authored-by: Stacy Kvernmo --- source/assets/js/playground.ts | 30 +++++- source/assets/js/playground/console-utils.ts | 96 ++++++++++++++----- source/assets/js/playground/utils.ts | 13 +-- .../assets/sass/components/_playground.scss | 74 ++++++++++---- source/assets/sass/config/color/_content.scss | 45 ++++++--- 5 files changed, 189 insertions(+), 69 deletions(-) diff --git a/source/assets/js/playground.ts b/source/assets/js/playground.ts index 0cff7a3c6..e8ec036f4 100644 --- a/source/assets/js/playground.ts +++ b/source/assets/js/playground.ts @@ -14,6 +14,7 @@ import { } from './playground/editor-setup.js'; import { ParseResult, + PlaygroundSelection, PlaygroundState, customLoader, deserializeState, @@ -104,9 +105,7 @@ function setupPlayground(): void { * Returns a playground state selection for the current single non-empty * selection, or `null` otherwise. */ - function editorSelectionToStateSelection(): - | PlaygroundState['selection'] - | null { + function editorSelectionToStateSelection(): PlaygroundSelection { const sel = editor.state.selection; if (sel.ranges.length !== 1) return null; @@ -123,7 +122,7 @@ function setupPlayground(): void { ]; } - /** Updates the editor's selection based on `playgroundState.selection`. */ + /** Updates the {@link editor}'s selection based on `{@link playgroundState.selection}`. */ function updateSelection(): void { if (playgroundState.selection === null) { const sel = editor.state.selection; @@ -155,6 +154,13 @@ function setupPlayground(): void { } } + /** Highlights {@link selection} and focuses on the {@link editor}. */ + function goToSelection(selection: PlaygroundSelection): void { + playgroundState.selection = selection; + updateSelection(); + editor.focus(); + } + // Apply initial state to dom function applyInitialState(): void { updateButtonState(); @@ -258,8 +264,22 @@ function setupPlayground(): void { '.sl-c-playground__console' ) as HTMLDivElement; console.innerHTML = playgroundState.debugOutput - .map(displayForConsoleLog) + .map(item => displayForConsoleLog(item, playgroundState)) .join('\n'); + console.querySelectorAll('a.console-location').forEach(link => { + (link as HTMLAnchorElement).addEventListener('click', event => { + if (!(event.metaKey || event.altKey || event.shiftKey)) { + event.preventDefault(); + } + const range = (event.currentTarget as HTMLAnchorElement).dataset.range + ?.split(',') + .map(n => parseInt(n)); + if (range && range.length === 4) { + const [fromL, fromC, toL, toC] = range; + goToSelection([fromL, fromC, toL, toC]); + } + }); + }); } function updateDiagnostics(): void { diff --git a/source/assets/js/playground/console-utils.ts b/source/assets/js/playground/console-utils.ts index b2a57088b..106f47a4d 100644 --- a/source/assets/js/playground/console-utils.ts +++ b/source/assets/js/playground/console-utils.ts @@ -1,5 +1,7 @@ import {Exception, SourceSpan} from 'sass'; +import {PlaygroundSelection, PlaygroundState, serializeState} from './utils'; + export interface ConsoleLogDebug { options: { span: SourceSpan; @@ -13,6 +15,9 @@ export interface ConsoleLogWarning { deprecation: boolean; span?: SourceSpan | undefined; stack?: string | undefined; + deprecationType?: { + id: string; + }; }; message: string; type: 'warn'; @@ -39,42 +44,81 @@ function encodeHTML(message: string): string { return el.innerHTML; } -function lineNumberFormatter(number?: number): string { - if (number === undefined) return ''; - number = number + 1; - return `${number}`; +// Returns undefined if no range, or a link to the state, including range. +function selectionLink( + playgroundState: PlaygroundState, + range: PlaygroundSelection +): string | undefined { + if (!range) return undefined; + return serializeState({...playgroundState, selection: range}); } -export function displayForConsoleLog(item: ConsoleLog): string { - const data: {type: string; lineNumber?: number; message: string} = { - type: item.type, - lineNumber: undefined, - message: '', - }; +// Returns a safe HTML string for a console item. +export function displayForConsoleLog( + item: ConsoleLog, + playgroundState: PlaygroundState +): string { + let lineNumber: number | undefined; + let message: string; + let range: PlaygroundSelection = null; + if (item.type === 'error') { if (item.error instanceof Exception) { - data.lineNumber = item.error.span.start.line; + const span = item.error.span; + lineNumber = span.start.line; + range = [ + span.start.line + 1, + span.start.column + 1, + span.end.line + 1, + span.end.column + 1, + ]; } - data.message = item.error?.toString() || ''; - } else if (['debug', 'warn'].includes(item.type)) { - data.message = item.message; - let lineNumber = item.options.span?.start?.line; - if (typeof lineNumber === 'undefined') { - const stack = 'stack' in item.options ? item.options.stack : ''; - const needleFromStackRegex = /^- (\d+):/; - const match = stack?.match(needleFromStackRegex); - if (match?.[1]) { + message = encodeHTML(item.error?.toString() ?? ''); + } else { + message = encodeHTML(item.message); + if (item.options.span) { + const span = item.options.span; + lineNumber = span.start.line; + range = [ + span.start.line + 1, + span.start.column + 1, + span.end.line + 1, + span.end.column + 1, + ]; + } else if ('stack' in item.options) { + const match = item.options.stack?.match(/^- (\d+):(\d+) /); + if (match) { // Stack trace starts at 1, all others come from span, which starts at // 0, so adjust before formatting. lineNumber = parseInt(match[1]) - 1; + range = [ + parseInt(match[1]), + parseInt(match[2]), + parseInt(match[1]), + parseInt(match[2]), + ]; } } - data.lineNumber = lineNumber; + + if (item.type === 'warn' && item.options.deprecationType?.id) { + const safeLink = `https://sass-lang.com/d/${item.options.deprecationType.id}`; + message = message.replace( + safeLink, + `${safeLink}` + ); + } } + const link = selectionLink(playgroundState, range); + + const locationStart = link + ? `` + : ''; - return `
@${data.type}:${lineNumberFormatter( - data.lineNumber - )}
${encodeHTML(data.message)}
`; + return `
${locationStart}@${item.type}${ + lineNumber !== undefined ? `:${lineNumber + 1}` : '' + }${locationEnd}
${message}
`; } diff --git a/source/assets/js/playground/utils.ts b/source/assets/js/playground/utils.ts index 5306c5566..31482ce02 100644 --- a/source/assets/js/playground/utils.ts +++ b/source/assets/js/playground/utils.ts @@ -7,18 +7,19 @@ import {ConsoleLog, ConsoleLogDebug, ConsoleLogWarning} from './console-utils'; const PLAYGROUND_LOAD_ERROR_MESSAGE = 'The Sass Playground does not support loading stylesheets.'; +/** + * `[fromLine, fromColumn, toLine, toColumn]`; all 1-indexed. If this is null, + * the editor has no selection. + */ +export type PlaygroundSelection = [number, number, number, number] | null; + export interface PlaygroundState { inputFormat: Exclude; outputFormat: OutputStyle; inputValue: string; compilerHasError: boolean; debugOutput: ConsoleLog[]; - - /** - * `[fromLine, fromColumn, toLine, toColumn]`; all 1-indexed. If this is null, - * the editor has no selection. - */ - selection: [number, number, number, number] | null; + selection: PlaygroundSelection; } export function serializeState(state: PlaygroundState): string { diff --git a/source/assets/sass/components/_playground.scss b/source/assets/sass/components/_playground.scss index 891d7162d..9d5f659fa 100644 --- a/source/assets/sass/components/_playground.scss +++ b/source/assets/sass/components/_playground.scss @@ -2,6 +2,12 @@ @use '../config'; @use '../config/color/brand'; +$playground-base-colors: ( + 'info': var(--sl-color--code-info), + 'warning': var(--sl-color--code-warning), + 'error': var(--sl-color--code-error), +); + .playground { --sl-max-width--container: 100vw; @@ -169,12 +175,13 @@ overflow-y: inherit; .cm-gutters { - background-color: var(--sl-background--editor); + background-color: transparent; border-right: none; } .cm-lineNumbers .cm-gutterElement { min-width: var(--sl-gutter--double); + padding: 0 0.5ch 0 1.5ch; } .cm-content, @@ -192,11 +199,26 @@ .cm-line { padding-left: var(--sl-gutter); + + &::before { + content: '\2022'; + color: var(--sl-color--bullet-line, transparent); + font-size: var(--sl-font-size--x-large); + left: 0; + position: absolute; + transform: translateY(-25%); + } + + @each $name, $color in $playground-base-colors { + &:has(.cm-lintPoint-#{$name}, .cm-lintRange-#{$name}) { + --sl-color--bullet-line: #{$color}; + } + } } .cm-activeLineGutter, .cm-activeLine { - background-color: var(--sl-color--warning-highlight); + background-color: var(--sl-color--code-highlight-light); [data-code='compiled'] & { background-color: var(--sl-color--code-background); @@ -213,21 +235,24 @@ } } - .cm-diagnostic { - color: var(--sl-color--code-text); - background: var(--sl-color--code-background-darker); + .cm-tooltip { + border: none; } - .cm-diagnostic-error { - border-color: var(--sl-color--error); + .cm-diagnostic { + background: var(--sl-color--background-tooltip); + border: 1px solid var(--sl-color--border-tooltip); + color: var(--sl-color--code-text); + padding: var(--sl-gutter--half); } - .cm-diagnostic-warning { - border-color: var(--sl-color--warn); - } + @each $name, $color in $playground-base-colors { + .cm-diagnostic-#{$name} { + --sl-color--border-tooltip: #{$color}; + --sl-color--background-tooltip: var(--sl-color--code-#{$name}-light); - .cm-diagnostic-info { - border-color: var(--sl-color--success); + border-color: $color; + } } .cm-specialChar { @@ -257,26 +282,41 @@ .sl-c-playground__console { font-family: var(--sl-font-family--code); + display: grid; + gap: var(--sl-gutter); + grid-auto-rows: max-content; + grid-template-columns: [location] auto [message] 1fr; height: 100%; line-height: 1; margin: 0; .console-line { + --sl-background--link: transparent; + --sl-border-color--link: transparent; + --sl-border-color--link-state: var(--sl-color--iron); + display: grid; - gap: var(--sl-gutter); - grid-template: 'location message' auto / 10ch 1fr; + grid-column: 1 / -1; + grid-template-columns: subgrid; margin-bottom: var(--sl-gutter--half); + place-items: start; } .console-message { display: grid; line-height: var(--sl-line-height--console); + + a { + justify-self: start; + } } + // Debug panel uses Sass terms "warn" and "debug" + // Code Mirror uses "warning" and "info" $console-type-colors: ( - 'error': var(--sl-color--error), - 'warn': var(--sl-color--warn), - 'debug': var(--sl-color--success), + 'error': var(--sl-color--code-error), + 'warn': var(--sl-color--code-warning), + 'debug': var(--sl-color--code-info), ); @each $name, $color in $console-type-colors { diff --git a/source/assets/sass/config/color/_content.scss b/source/assets/sass/config/color/_content.scss index 0cc103522..5ce37a2dd 100644 --- a/source/assets/sass/config/color/_content.scss +++ b/source/assets/sass/config/color/_content.scss @@ -1,14 +1,28 @@ @use 'sass:color'; @use 'brand'; -$sl-color--highlight: color.adjust(brand.$sl-color--hopbush, $lightness: -10%); $sl-color--action: color.adjust(brand.$sl-color--bouquet, $lightness: -10%); -$sl-color--shadow: rgba(brand.$sl-color--midnight-blue, 0.125); $sl-color--active: color.adjust(brand.$sl-color--venus, $lightness: -10%); +$sl-color--highlight: color.adjust(brand.$sl-color--hopbush, $lightness: -10%); +$sl-color--link-action: rgba(218, 219, 223, 25%); +$sl-color--shadow: rgba(brand.$sl-color--midnight-blue, 0.125); + +// Callouts/Info Panels +$sl-color--warning-light: color.adjust( + brand.$sl-color--hopbush, + $lightness: 27% +); +$sl-color--warning-lighter: color.adjust( + brand.$sl-color--hopbush, + $lightness: 38% +); +$sl-color--info-light: color.adjust(brand.$sl-color--patina, $lightness: 32%); +$sl-color--info-lighter: color.adjust(brand.$sl-color--patina, $lightness: 47%); + +// Code Colors $sl-color--code-background: #f8f8f8; $sl-color--code-background-darker: #ebebeb; $sl-color--code-text: color.adjust(brand.$sl-color--pale-sky, $lightness: -25%); -$sl-color--link-action: rgba(218, 219, 223, 25%); $sl-color--code-warm: #cf1666; $sl-color--code-bright-dark: #900; $sl-color--code-bright: #df1144; @@ -17,17 +31,18 @@ $sl-color--code-muted: #656556; $sl-color--code-base: #066; $sl-color--code-cool: #458; $sl-color--code-dark: black; -$sl-color--warning-light: color.adjust( - brand.$sl-color--hopbush, - $lightness: 27% +$sl-color--code-highlight-light: rgba($sl-color--warning-light, 0.2); + +// Playground Status +$sl-color--code-error: #cf0254; +$sl-color--code-error-light: color.adjust( + $sl-color--code-error, + $lightness: 55% ); -$sl-color--warning-lighter: color.adjust( - brand.$sl-color--hopbush, - $lightness: 38% +$sl-color--code-warning: #c14e00; +$sl-color--code-warning-light: color.adjust( + $sl-color--code-warning, + $lightness: 55% ); -$sl-color--warning-highlight: rgba($sl-color--warning-light, 0.2); -$sl-color--info-light: color.adjust(brand.$sl-color--patina, $lightness: 32%); -$sl-color--info-lighter: color.adjust(brand.$sl-color--patina, $lightness: 47%); -$sl-color--error: #cf0254; -$sl-color--warn: #c14e00; -$sl-color--success: #168073; +$sl-color--code-info: #168073; +$sl-color--code-info-light: color.adjust($sl-color--code-info, $lightness: 65%);