Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Playground] Debug links #1179

Merged
merged 22 commits into from
Sep 19, 2024
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
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();
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
}
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