Skip to content

Commit

Permalink
[Playground] Debug links (#1179)
Browse files Browse the repository at this point in the history
Co-authored-by: Stacy Kvernmo <stacy@oddbird.net>
  • Loading branch information
jamesnw and stacyk committed Sep 19, 2024
1 parent a9b0cd1 commit efacf6c
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 69 deletions.
30 changes: 25 additions & 5 deletions source/assets/js/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from './playground/editor-setup.js';
import {
ParseResult,
PlaygroundSelection,
PlaygroundState,
customLoader,
deserializeState,
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down
96 changes: 70 additions & 26 deletions source/assets/js/playground/console-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {Exception, SourceSpan} from 'sass';

import {PlaygroundSelection, PlaygroundState, serializeState} from './utils';

export interface ConsoleLogDebug {
options: {
span: SourceSpan;
Expand All @@ -13,6 +15,9 @@ export interface ConsoleLogWarning {
deprecation: boolean;
span?: SourceSpan | undefined;
stack?: string | undefined;
deprecationType?: {
id: string;
};
};
message: string;
type: 'warn';
Expand All @@ -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,
`<a href="${safeLink}" target="_blank">${safeLink}</a>`
);
}
}
const link = selectionLink(playgroundState, range);

const locationStart = link
? `<a href="#${link}" class="console-location" data-range=${range}>`
: '<div class="console-location">';

const locationEnd = link ? '</a>' : '</div>';

return `<div class="console-line"><div class="console-location"><span class="console-type console-type-${
data.type
}">@${data.type}</span>:${lineNumberFormatter(
data.lineNumber
)}</div><div class="console-message">${encodeHTML(data.message)}</div></div>`;
return `<div class="console-line">${locationStart}<span class="console-type console-type-${
item.type
}">@${item.type}</span>${
lineNumber !== undefined ? `:${lineNumber + 1}` : ''
}${locationEnd}<div class="console-message">${message}</div></div>`;
}
13 changes: 7 additions & 6 deletions source/assets/js/playground/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Syntax, 'css'>;
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 {
Expand Down
74 changes: 57 additions & 17 deletions source/assets/sass/components/_playground.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit efacf6c

Please sign in to comment.