Skip to content

Commit

Permalink
cherry-pick(#21856): chore: pack codemirror on resize
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Mar 22, 2023
1 parent 8693fd4 commit 08422f0
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 82 deletions.
3 changes: 1 addition & 2 deletions packages/trace-viewer/src/ui/filmStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
import './filmStrip.css';
import type { Boundaries, Size } from '../geometry';
import * as React from 'react';
import { useMeasure } from './helpers';
import { upperBound } from '@web/uiUtils';
import { useMeasure, upperBound } from '@web/uiUtils';
import type { PageEntry } from '../entries';
import type { MultiTraceModel } from './modelUtil';

Expand Down
55 changes: 0 additions & 55 deletions packages/trace-viewer/src/ui/helpers.tsx

This file was deleted.

3 changes: 1 addition & 2 deletions packages/trace-viewer/src/ui/snapshotTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@

import './snapshotTab.css';
import * as React from 'react';
import { useMeasure } from './helpers';
import type { ActionTraceEvent } from '@trace/trace';
import { context, prevInList } from './modelUtil';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton } from '@web/components/toolbarButton';
import { copy } from '@web/uiUtils';
import { copy, useMeasure } from '@web/uiUtils';
import { InjectedScript } from '@injected/injectedScript';
import { Recorder } from '@injected/recorder';
import { asLocator } from '@isomorphic/locatorGenerators';
Expand Down
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/ui/sourceTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import type { ActionTraceEvent } from '@trace/trace';
import { SplitView } from '@web/components/splitView';
import * as React from 'react';
import { useAsyncMemo } from './helpers';
import { useAsyncMemo } from '@web/uiUtils';
import './sourceTab.css';
import { StackTraceView } from './stackTrace';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
Expand Down
3 changes: 1 addition & 2 deletions packages/trace-viewer/src/ui/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@
*/

import type { ActionTraceEvent, EventTraceEvent } from '@trace/trace';
import { msToString } from '@web/uiUtils';
import { msToString, useMeasure } from '@web/uiUtils';
import * as React from 'react';
import type { Boundaries } from '../geometry';
import { FilmStrip } from './filmStrip';
import { useMeasure } from './helpers';
import type { MultiTraceModel } from './modelUtil';
import './timeline.css';

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/components/DEPS.list
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[*]
../theme.ts
../third_party/vscode/codicon.css
../uiUtils.ts

[expandable.spec.tsx]
***
Expand Down
8 changes: 7 additions & 1 deletion packages/web/src/components/codeMirrorWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import './codeMirrorWrapper.css';
import * as React from 'react';
import type { CodeMirror } from './codeMirrorModule';
import { ansi2htmlMarkup } from './errorMessage';
import { useMeasure } from '../uiUtils';

export type SourceHighlight = {
line: number;
Expand Down Expand Up @@ -51,7 +52,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
wrapLines,
onChange,
}) => {
const codemirrorElement = React.useRef<HTMLDivElement>(null);
const [measure, codemirrorElement] = useMeasure<HTMLDivElement>();
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default));
const codemirrorRef = React.useRef<{ cm: CodeMirror.Editor, highlight?: SourceHighlight[], widgets?: CodeMirror.LineWidget[] } | null>(null);
const [codemirror, setCodemirror] = React.useState<CodeMirror.Editor>();
Expand Down Expand Up @@ -98,6 +99,11 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
})();
}, [modulePromise, codemirror, codemirrorElement, language, lineNumbers, wrapLines, readOnly]);

React.useEffect(() => {
if (codemirrorRef.current)
codemirrorRef.current.cm.setSize(measure.width, measure.height);
}, [measure]);

React.useEffect(() => {
if (!codemirror)
return;
Expand Down
36 changes: 18 additions & 18 deletions packages/web/src/components/xtermWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { ITheme, Terminal } from 'xterm';
import type { FitAddon } from 'xterm-addon-fit';
import type { XtermModule } from './xtermModule';
import { currentTheme, addThemeListener, removeThemeListener } from '@web/theme';
import { useMeasure } from '@web/uiUtils';

export type XtermDataSource = {
pending: (string | Uint8Array)[];
Expand All @@ -31,10 +32,10 @@ export type XtermDataSource = {
export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({
source,
}) => {
const xtermElement = React.useRef<HTMLDivElement>(null);
const [measure, xtermElement] = useMeasure<HTMLDivElement>();
const [theme, setTheme] = React.useState(currentTheme());
const [modulePromise] = React.useState<Promise<XtermModule>>(import('./xtermModule').then(m => m.default));
const terminal = React.useRef<{ terminal: Terminal, fitAddon: FitAddon }| null>(null);
const terminal = React.useRef<{ terminal: Terminal, fitAddon: FitAddon } | null>(null);

React.useEffect(() => {
addThemeListener(setTheme);
Expand All @@ -44,7 +45,6 @@ export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({
React.useEffect(() => {
const oldSourceWrite = source.write;
const oldSourceClear = source.clear;
let resizeObserver: ResizeObserver | undefined;

(async () => {
// Always load the module first.
Expand All @@ -53,15 +53,19 @@ export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({
if (!element)
return;

if (terminal.current && terminal)
const terminalTheme = theme === 'dark-mode' ? darkTheme : lightTheme;
if (terminal.current && terminal.current.terminal.options.theme === terminalTheme)
return;

if (terminal.current)
element.textContent = '';

const newTerminal = new Terminal({
convertEol: true,
fontSize: 13,
scrollback: 10000,
fontFamily: 'var(--vscode-editor-font-family)',
theme: theme === 'dark-mode' ? darkTheme : lightTheme
theme: terminalTheme,
});

const fitAddon = new FitAddon();
Expand All @@ -79,27 +83,23 @@ export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({
newTerminal.open(element);
fitAddon.fit();
terminal.current = { terminal: newTerminal, fitAddon };
resizeObserver = new ResizeObserver(() => {
// Fit reads data from the terminal itself, which updates lazily, probably on some timer
// or mutation observer. Work around it.
setTimeout(() => {
fitAddon.fit();
source.resize(newTerminal.cols, newTerminal.rows);
}, 100);
});
resizeObserver.observe(element);
})();
return () => {
source.clear = oldSourceClear;
source.write = oldSourceWrite;
resizeObserver?.disconnect();
};
}, [modulePromise, terminal, xtermElement, source, theme]);

React.useEffect(() => {
if (terminal.current)
terminal.current.terminal.options.theme = theme === 'dark-mode' ? darkTheme : lightTheme;
}, [theme]);
// Fit reads data from the terminal itself, which updates lazily, probably on some timer
// or mutation observer. Work around it.
setTimeout(() => {
if (!terminal.current)
return;
terminal.current.fitAddon.fit();
source.resize(terminal.current.terminal.cols, terminal.current.terminal.rows);
}, 250);
}, [measure, source]);

return <div data-testid='output' className='xterm-wrapper' style={{ flex: 'auto' }} ref={xtermElement}></div>;
};
Expand Down
38 changes: 38 additions & 0 deletions packages/web/src/uiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,44 @@

import React from 'react';

// Recalculates the value when dependencies change.
export function useAsyncMemo<T>(fn: () => Promise<T>, deps: React.DependencyList, initialValue: T, resetValue?: T) {
const [value, setValue] = React.useState<T>(initialValue);
React.useEffect(() => {
let canceled = false;
if (resetValue !== undefined)
setValue(resetValue);
fn().then(value => {
if (!canceled)
setValue(value);
});
return () => {
canceled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return value;
}

// Tracks the element size and returns it's contentRect (always has x=0, y=0).
export function useMeasure<T extends Element>() {
const ref = React.useRef<T | null>(null);
const [measure, setMeasure] = React.useState(new DOMRect(0, 0, 10, 10));
React.useLayoutEffect(() => {
const target = ref.current;
if (!target)
return;
const resizeObserver = new ResizeObserver((entries: any) => {
const entry = entries[entries.length - 1];
if (entry && entry.contentRect)
setMeasure(entry.contentRect);
});
resizeObserver.observe(target);
return () => resizeObserver.disconnect();
}, [ref]);
return [measure, ref] as const;
}

export function msToString(ms: number): string {
if (!isFinite(ms))
return '-';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { test, expect } from './ui-mode-fixtures';

test.describe.configure({ mode: 'parallel' });

test('should list tests', async ({ runUITest }) => {
test('should print load errors', async ({ runUITest }) => {
const page = await runUITest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
Expand All @@ -30,3 +30,29 @@ test('should list tests', async ({ runUITest }) => {
await page.getByTitle('Toggle output').click();
await expect(page.getByTestId('output')).toContainText(`Unexpected reserved word 'await'`);
});

test('should work after theme switch', async ({ runUITest, writeFiles }) => {
const page = await runUITest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('syntax error', async () => {
console.log('Hello world 1');
});
`,
});
await page.getByTitle('Toggle output').click();
await page.getByTitle('Run all').click();
await expect(page.getByTestId('output')).toContainText(`Hello world 1`);

await page.getByTitle('Toggle color mode').click();
writeFiles({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('syntax error', async () => {
console.log('Hello world 2');
});
`,
});
await page.getByTitle('Run all').click();
await expect(page.getByTestId('output')).toContainText(`Hello world 2`);
});

0 comments on commit 08422f0

Please sign in to comment.