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 16 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
28 changes: 24 additions & 4 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 Down Expand Up @@ -155,6 +154,13 @@ function setupPlayground(): void {
}
}

/** Updates state, applies selection in editor, and focus editor. */
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
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
105 changes: 86 additions & 19 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,104 @@ function encodeHTML(message: string): string {
return el.innerHTML;
}

// Converts 0-indexed number to 1-indexed string, or empty string if undefined.
function lineNumberFormatter(number?: number): string {
if (number === undefined) return '';
number = number + 1;
return `${number}`;
}

export function displayForConsoleLog(item: ConsoleLog): string {
const data: {type: string; lineNumber?: number; message: string} = {
type: item.type,
lineNumber: undefined,
message: '',
};
// 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});
}

// Returns a safe HTML string for a console item.
export function displayForConsoleLog(
item: ConsoleLog,
playgroundState: PlaygroundState
): string {
const type = item.type;
let lineNumber = undefined;
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
let message = '';
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
let safeLink = undefined;
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
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() || '';
message = item.error?.toString() || '';
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
} else if (['debug', 'warn'].includes(item.type)) {
data.message = item.message;
let lineNumber = item.options.span?.start?.line;
if (typeof lineNumber === 'undefined') {
message = item.message;
let _lineNumber = item.options.span?.start?.line;
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
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,
];
}

if (typeof _lineNumber === 'undefined') {
const stack = 'stack' in item.options ? item.options.stack : '';
const needleFromStackRegex = /^- (\d+):/;
const needleFromStackRegex = /^- (\d+):(\d+) /;
const match = stack?.match(needleFromStackRegex);
if (match?.[1]) {
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
// Stack trace starts at 1, all others come from span, which starts at
// 0, so adjust before formatting.
lineNumber = parseInt(match[1]) - 1;
_lineNumber = parseInt(match[1]) - 1;
}
if (match?.[2]) {
range = [
parseInt(match[1]),
parseInt(match[2]),
parseInt(match[1]),
parseInt(match[2]),
];
}
}
data.lineNumber = lineNumber;
lineNumber = _lineNumber;

if (item.type === 'warn' && item.options.deprecationType?.id) {
safeLink = `https://sass-lang.com/d/${item.options.deprecationType.id}`;
}
}
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>';

let html = encodeHTML(message);

if (safeLink) {
// Wrap text link in anchor link
html = html.replace(
safeLink,
`<a href="${safeLink}" target="_blank">${safeLink}</a>`
);
}

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-${
type
}">@${type}</span>:${lineNumberFormatter(
lineNumber
)}${locationEnd}<div class="console-message">${html}</div></div>`;
jamesnw marked this conversation as resolved.
Show resolved Hide resolved
}
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