diff --git a/package-lock.json b/package-lock.json index dda5691907..4e342aa23c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6203,14 +6203,12 @@ "version": "0.0.0" }, "packages/trace-viewer": { - "version": "0.0.0", - "dependencies": { - "ansi-to-html": "^0.7.2" - } + "version": "0.0.0" }, "packages/web": { "version": "0.0.0", "dependencies": { + "ansi-to-html": "^0.7.2", "codemirror": "^5.65.9", "xterm": "^5.1.0", "xterm-addon-fit": "^0.7.0" @@ -9704,10 +9702,7 @@ } }, "trace-viewer": { - "version": "file:packages/trace-viewer", - "requires": { - "ansi-to-html": "^0.7.2" - } + "version": "file:packages/trace-viewer" }, "tree-kill": { "version": "1.2.2", @@ -9886,6 +9881,7 @@ "web": { "version": "file:packages/web", "requires": { + "ansi-to-html": "^0.7.2", "codemirror": "^5.65.9", "xterm": "^5.1.0", "xterm-addon-fit": "^0.7.0" diff --git a/packages/recorder/src/callLog.css b/packages/recorder/src/callLog.css index d9db66f3c6..4f6da99844 100644 --- a/packages/recorder/src/callLog.css +++ b/packages/recorder/src/callLog.css @@ -56,14 +56,14 @@ } .call-log-call.error { - background-color: #fff0f0; + background-color: var(--vscode-inputValidation-errorBackground); border-top: 1px solid var(--vscode-panel-border); } .call-log-call.error .call-log-call-header, .call-log-message.error, .call-log .codicon-error { - color: red; + color: var(--vscode-errorForeground); } .call-log-details { diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index da5d628706..1662b665b7 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -16,7 +16,6 @@ import type { CallLog, Mode, Source } from './recorderTypes'; import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; -import { Source as SourceView } from '@web/components/source'; import { SplitView } from '@web/components/splitView'; import { Toolbar } from '@web/components/toolbar'; import { ToolbarButton } from '@web/components/toolbarButton'; @@ -134,7 +133,7 @@ export const Recorder: React.FC = ({ toggleTheme()}> - +
{ @@ -143,7 +142,7 @@ export const Recorder: React.FC = ({ { setLocator(text); window.dispatch({ event: 'selectorUpdated', params: { selector: text, language: source.language } }); - }}> + }} /> { copy(locator); }}> diff --git a/packages/trace-viewer/package.json b/packages/trace-viewer/package.json index ba6c550df3..069d430495 100644 --- a/packages/trace-viewer/package.json +++ b/packages/trace-viewer/package.json @@ -7,8 +7,5 @@ "build": "vite build && tsc", "build-sw": "vite --config vite.sw.config.ts build && tsc", "preview": "vite preview" - }, - "dependencies": { - "ansi-to-html": "^0.7.2" } } diff --git a/packages/trace-viewer/src/ui/actionList.css b/packages/trace-viewer/src/ui/actionList.css index 3332e0a7fd..a3f64244e9 100644 --- a/packages/trace-viewer/src/ui/actionList.css +++ b/packages/trace-viewer/src/ui/actionList.css @@ -45,11 +45,11 @@ } .action-icons:hover { - border-bottom: 1px solid white; + border-bottom: 1px solid var(--vscode-sideBarTitle-foreground); } .action-error { - color: red; + color: var(--vscode-errorForeground); position: relative; margin-right: 2px; flex: none; diff --git a/packages/trace-viewer/src/ui/callTab.css b/packages/trace-viewer/src/ui/callTab.css index 522a1fae87..a0f9f2a116 100644 --- a/packages/trace-viewer/src/ui/callTab.css +++ b/packages/trace-viewer/src/ui/callTab.css @@ -28,7 +28,7 @@ } .call-error .codicon { - color: red; + color: var(--vscode-errorForeground); position: relative; top: 2px; margin-right: 2px; diff --git a/packages/trace-viewer/src/ui/callTab.tsx b/packages/trace-viewer/src/ui/callTab.tsx index d5a0171127..219587cddb 100644 --- a/packages/trace-viewer/src/ui/callTab.tsx +++ b/packages/trace-viewer/src/ui/callTab.tsx @@ -22,7 +22,7 @@ import './callTab.css'; import { CopyToClipboard } from './copyToClipboard'; import { asLocator } from '@isomorphic/locatorGenerators'; import type { Language } from '@isomorphic/locatorGenerators'; -import { ErrorMessage } from './errorMessage'; +import { ErrorMessage } from '@web/components/errorMessage'; export const CallTab: React.FunctionComponent<{ action: ActionTraceEvent | undefined, diff --git a/packages/trace-viewer/src/ui/consoleTab.css b/packages/trace-viewer/src/ui/consoleTab.css index 8546ad98b0..156ca76d86 100644 --- a/packages/trace-viewer/src/ui/consoleTab.css +++ b/packages/trace-viewer/src/ui/consoleTab.css @@ -33,16 +33,16 @@ } .console-line.error { - background: #fff0f0; - border-top-color: #ffd6d6; - border-bottom-color: #ffd6d6; - color: red; + background: var(--vscode-inputValidation-errorBackground); + border-top-color: var(--vscode-inputValidation-errorBorder); + border-bottom-color: var(--vscode-inputValidation-errorBorder); + color: var(--vscode-errorForeground); } .console-line.warning { - background: #fffbe5; - border-top-color: #fff5c2; - border-bottom-color: #fff5c2; + background: var(--vscode-inputValidation-warningBackground); + border-top-color: var(--vscode-inputValidation-warningBorder); + border-bottom-color: var(--vscode-inputValidation-warningBorder); } .console-line .codicon { diff --git a/packages/trace-viewer/src/ui/sourceTab.tsx b/packages/trace-viewer/src/ui/sourceTab.tsx index ff81a4fb62..cca9e06222 100644 --- a/packages/trace-viewer/src/ui/sourceTab.tsx +++ b/packages/trace-viewer/src/ui/sourceTab.tsx @@ -16,14 +16,14 @@ import type { StackFrame } from '@protocol/channels'; import type { ActionTraceEvent } from '@trace/trace'; -import { Source as SourceView } from '@web/components/source'; import { SplitView } from '@web/components/splitView'; import * as React from 'react'; import { useAsyncMemo } from './helpers'; import './sourceTab.css'; import { StackTraceView } from './stackTrace'; +import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; -type StackInfo = string | { +type StackInfo = { frames: StackFrame[]; fileContent: Map; }; @@ -33,17 +33,15 @@ export const SourceTab: React.FunctionComponent<{ }> = ({ action }) => { const [lastAction, setLastAction] = React.useState(); const [selectedFrame, setSelectedFrame] = React.useState(0); - const [needReveal, setNeedReveal] = React.useState(false); if (lastAction !== action) { setLastAction(action); setSelectedFrame(0); - setNeedReveal(true); } const stackInfo = React.useMemo(() => { if (!action) - return ''; + return { frames: [], fileContent: new Map() }; const frames = action.stack || []; return { frames, @@ -52,34 +50,20 @@ export const SourceTab: React.FunctionComponent<{ }, [action]); const content = useAsyncMemo(async () => { - let value: string; - if (typeof stackInfo === 'string') { - value = stackInfo; - } else { - const filePath = stackInfo.frames[selectedFrame]?.file; - if (!filePath) - return ''; - if (!stackInfo.fileContent.has(filePath)) { - const sha1 = await calculateSha1(filePath); - stackInfo.fileContent.set(filePath, await fetch(`sha1/src@${sha1}.txt`).then(response => response.text()).catch(e => ``)); - } - value = stackInfo.fileContent.get(filePath)!; + const filePath = stackInfo.frames[selectedFrame]?.file; + if (!filePath) + return ''; + if (!stackInfo.fileContent.has(filePath)) { + const sha1 = await calculateSha1(filePath); + stackInfo.fileContent.set(filePath, await fetch(`sha1/src@${sha1}.txt`).then(response => response.text()).catch(() => ``)); } - return value; + return stackInfo.fileContent.get(filePath)!; }, [stackInfo, selectedFrame], ''); - const targetLine = typeof stackInfo === 'string' ? 0 : stackInfo.frames[selectedFrame]?.line || 0; - - const targetLineRef = React.useRef(null); - React.useLayoutEffect(() => { - if (needReveal && targetLineRef.current) { - targetLineRef.current.scrollIntoView({ block: 'center', inline: 'nearest' }); - setNeedReveal(false); - } - }, [needReveal, targetLineRef]); - + const targetLine = stackInfo.frames[selectedFrame]?.line || 0; + const error = action?.error?.message; return - + ; }; diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 7c906abe6f..8358896b69 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -49,9 +49,9 @@ export const Workbench: React.FunctionComponent<{ const tabs: TabbedPaneTabModel[] = [ { id: 'call', title: 'Call', render: () => }, + { id: 'source', title: 'Source', count: 0, render: () => }, { id: 'console', title: 'Console', count: consoleCount, render: () => }, { id: 'network', title: 'Network', count: networkCount, render: () => }, - { id: 'source', title: 'Source', count: 0, render: () => }, ]; if (output) diff --git a/packages/web/package.json b/packages/web/package.json index 2b21846455..fd68fcb78d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "scripts": {}, "dependencies": { + "ansi-to-html": "^0.7.2", "codemirror": "^5.65.9", "xterm": "^5.1.0", "xterm-addon-fit": "^0.7.0" diff --git a/packages/web/src/components/DEPS.list b/packages/web/src/components/DEPS.list index 0584bb6385..fd95d51366 100644 --- a/packages/web/src/components/DEPS.list +++ b/packages/web/src/components/DEPS.list @@ -5,7 +5,7 @@ [expandable.spec.tsx] *** -[source.spec.tsx] +[codeMirrorWrapper.spec.tsx] *** [splitView.spec.tsx] diff --git a/packages/web/src/components/codeMirrorWrapper.css b/packages/web/src/components/codeMirrorWrapper.css index f209c1260d..3243456c85 100644 --- a/packages/web/src/components/codeMirrorWrapper.css +++ b/packages/web/src/components/codeMirrorWrapper.css @@ -118,6 +118,7 @@ body.dark-mode .CodeMirror span.cm-type { .CodeMirror .CodeMirror-gutters { z-index: 0; + background: var(--vscode-editorGutter-background); } .CodeMirror .CodeMirror-gutterwrapper { @@ -138,3 +139,31 @@ body.dark-mode .CodeMirror span.cm-type { font-weight: var(--vscode-editor-font-weight) !important; font-size: var(--vscode-editor-font-size) !important; } + +.CodeMirror .source-line-running { + background-color: #b3dbff7f; + z-index: 2; +} + +.CodeMirror .source-line-paused { + background-color: #b3dbff7f; + outline: 1px solid #008aff; + z-index: 2; +} + +.CodeMirror .source-line-error { + /* Intentionally empty. */ +} + +.CodeMirror .source-line-error-underline { + text-decoration: underline wavy var(--vscode-errorForeground); + position: relative; + top: -12px; +} + +.CodeMirror .source-line-error-widget { + background-color: var(--vscode-inputValidation-errorBackground); + white-space: pre-wrap; + margin: 3px 10px; + padding: 5px; +} diff --git a/packages/web/src/components/source.spec.tsx b/packages/web/src/components/codeMirrorWrapper.spec.tsx similarity index 84% rename from packages/web/src/components/source.spec.tsx rename to packages/web/src/components/codeMirrorWrapper.spec.tsx index d363bfec3d..3fb630ae5d 100644 --- a/packages/web/src/components/source.spec.tsx +++ b/packages/web/src/components/codeMirrorWrapper.spec.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { expect, test } from '@playwright/experimental-ct-react'; -import { Source } from './source'; +import { CodeMirrorWrapper } from './codeMirrorWrapper'; test.use({ viewport: { width: 500, height: 500 } }); @@ -70,31 +70,31 @@ class Program `; test('highlight JavaScript', async ({ mount }) => { - const component = await mount(); + const component = await mount(); await expect(component.locator('text="async"').first()).toHaveClass('cm-keyword'); }); test('highlight Python', async ({ mount }) => { - const component = await mount(); + const component = await mount(); await expect(component.locator('text="async"').first()).toHaveClass('cm-keyword'); }); test('highlight Java', async ({ mount }) => { - const component = await mount(); + const component = await mount(); await expect(component.locator('text="public"').first()).toHaveClass('cm-keyword'); }); test('highlight C#', async ({ mount }) => { - const component = await mount(); + const component = await mount(); await expect(component.locator('text="public"').first()).toHaveClass('cm-keyword'); }); test('highlight lines', async ({ mount }) => { - const component = await mount(); + ]} />); await expect(component.locator('.source-line-running')).toContainText('goto'); await expect(component.locator('.source-line-paused')).toContainText('title'); await expect(component.locator('.source-line-error')).toContainText('expect'); diff --git a/packages/web/src/components/codeMirrorWrapper.tsx b/packages/web/src/components/codeMirrorWrapper.tsx index e44b83c0a3..dae38cf131 100644 --- a/packages/web/src/components/codeMirrorWrapper.tsx +++ b/packages/web/src/components/codeMirrorWrapper.tsx @@ -14,13 +14,15 @@ limitations under the License. */ -import './source.css'; +import './codeMirrorWrapper.css'; import * as React from 'react'; import type { CodeMirror } from './codeMirrorModule'; +import { ansi2htmlMarkup } from './errorMessage'; export type SourceHighlight = { line: number; type: 'running' | 'paused' | 'error'; + message?: string; }; export type Language = 'javascript' | 'python' | 'java' | 'csharp'; @@ -28,7 +30,7 @@ export type Language = 'javascript' | 'python' | 'java' | 'csharp'; export interface SourceProps { text: string; language: Language; - readOnly: boolean; + readOnly?: boolean; // 1-based highlight?: SourceHighlight[]; revealLine?: number; @@ -51,7 +53,7 @@ export const CodeMirrorWrapper: React.FC = ({ }) => { const codemirrorElement = React.useRef(null); const [modulePromise] = React.useState>(import('./codeMirrorModule').then(m => m.default)); - const codemirrorRef = React.useRef(null); + const codemirrorRef = React.useRef<{ cm: CodeMirror.Editor, highlight: SourceHighlight[], widgets: CodeMirror.LineWidget[] } | null>(null); const [codemirror, setCodemirror] = React.useState(); React.useEffect(() => { @@ -72,25 +74,25 @@ export const CodeMirrorWrapper: React.FC = ({ mode = 'text/x-csharp'; if (codemirrorRef.current - && mode === codemirrorRef.current.getOption('mode') - && readOnly === codemirrorRef.current.getOption('readOnly') - && lineNumbers === codemirrorRef.current.getOption('lineNumbers') - && wrapLines === codemirrorRef.current.getOption('lineWrapping')) { + && mode === codemirrorRef.current.cm.getOption('mode') + && !!readOnly === codemirrorRef.current.cm.getOption('readOnly') + && lineNumbers === codemirrorRef.current.cm.getOption('lineNumbers') + && wrapLines === codemirrorRef.current.cm.getOption('lineWrapping')) { // No need to re-create codemirror. return; } // Either configuration is different or we don't have a codemirror yet. - codemirrorRef.current?.getWrapperElement().remove(); + codemirrorRef.current?.cm?.getWrapperElement().remove(); const cm = CodeMirror(element, { value: '', mode, - readOnly, + readOnly: !!readOnly, lineNumbers, lineWrapping: wrapLines, }); - codemirrorRef.current = cm; + codemirrorRef.current = { cm, highlight: [], widgets: [] }; setCodemirror(cm); return cm; })(); @@ -113,10 +115,37 @@ export const CodeMirrorWrapper: React.FC = ({ codemirror.focus(); } } - for (let i = 0; i < codemirror.lineCount(); ++i) - codemirror.removeLineClass(i, 'wrap'); + + // Line highlight. + for (const h of codemirrorRef.current!.highlight) + codemirror.removeLineClass(h.line - 1, 'wrap'); for (const h of highlight || []) codemirror.addLineClass(h.line - 1, 'wrap', `source-line-${h.type}`); + codemirrorRef.current!.highlight = highlight || []; + + // Error widgets. + for (const w of codemirrorRef.current!.widgets) + codemirror.removeLineWidget(w); + const widgets: CodeMirror.LineWidget[] = []; + for (const h of highlight || []) { + if (h.type !== 'error') + continue; + + const line = codemirrorRef.current?.cm.getLine(h.line - 1); + if (line) { + const underlineWidgetElement = document.createElement('div'); + underlineWidgetElement.className = 'source-line-error-underline'; + underlineWidgetElement.innerHTML = ' '.repeat(line.length || 1); + widgets.push(codemirror.addLineWidget(h.line, underlineWidgetElement, { above: true, coverGutter: false })); + } + + const errorWidgetElement = document.createElement('div'); + errorWidgetElement.innerHTML = ansi2htmlMarkup(h.message || ''); + errorWidgetElement.className = 'source-line-error-widget'; + widgets.push(codemirror.addLineWidget(h.line, errorWidgetElement, { above: true, coverGutter: false })); + } + codemirrorRef.current!.widgets = widgets; + if (revealLine) codemirror.scrollIntoView({ line: revealLine - 1, ch: 0 }, 50); }, [codemirror, text, highlight, revealLine, focusOnChange, onChange]); diff --git a/packages/trace-viewer/src/ui/errorMessage.css b/packages/web/src/components/errorMessage.css similarity index 100% rename from packages/trace-viewer/src/ui/errorMessage.css rename to packages/web/src/components/errorMessage.css diff --git a/packages/trace-viewer/src/ui/errorMessage.tsx b/packages/web/src/components/errorMessage.tsx similarity index 80% rename from packages/trace-viewer/src/ui/errorMessage.tsx rename to packages/web/src/components/errorMessage.tsx index fca090dbfc..b2d0452c59 100644 --- a/packages/trace-viewer/src/ui/errorMessage.tsx +++ b/packages/web/src/components/errorMessage.tsx @@ -21,17 +21,19 @@ import './errorMessage.css'; export const ErrorMessage: React.FC<{ error: string; }> = ({ error }) => { - const html = React.useMemo(() => { - const config: any = { - bg: 'var(--vscode-panel-background)', - fg: 'var(--vscode-foreground)', - }; - config.colors = ansiColors; - return new ansi2html(config).toHtml(escapeHTML(error)); - }, [error]); + const html = React.useMemo(() => ansi2htmlMarkup(error), [error]); return
; }; +export function ansi2htmlMarkup(text: string) { + const config: any = { + bg: 'var(--vscode-panel-background)', + fg: 'var(--vscode-foreground)', + }; + config.colors = ansiColors; + return new ansi2html(config).toHtml(escapeHTML(text)); +} + const ansiColors = { 0: '#000', 1: '#C00', diff --git a/packages/web/src/components/source.css b/packages/web/src/components/source.css deleted file mode 100644 index e13a4f280c..0000000000 --- a/packages/web/src/components/source.css +++ /dev/null @@ -1,48 +0,0 @@ -/* - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -@import '../third_party/vscode/colors.css'; - -.source { - display: flex; - flex: auto; - flex-direction: column; - white-space: pre; - overflow: auto; - user-select: text; - font-family: var(--vscode-editor-font-family); - font-weight: var(--vscode-editor-font-weight); - line-height: 19px; - background: var(--vscode-editor-background); - color: var(--vscode-editor-foreground); -} - -.source-line-running { - background-color: #b3dbff7f; - z-index: 2; -} - -.source-line-paused { - background-color: #b3dbff7f; - outline: 1px solid #008aff; - z-index: 2; -} - -.source-line-error { - background-color: #fff0f0; - outline: 1px solid #ff5656; - z-index: 2; -} diff --git a/packages/web/src/components/source.tsx b/packages/web/src/components/source.tsx deleted file mode 100644 index 5b0bd7c403..0000000000 --- a/packages/web/src/components/source.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -import * as React from 'react'; -import './codeMirrorWrapper.css'; -import type { Language } from './codeMirrorWrapper'; -import { CodeMirrorWrapper } from './codeMirrorWrapper'; - -export type SourceHighlight = { - line: number; - type: 'running' | 'paused' | 'error'; -}; - -export interface SourceProps { - text: string; - language: Language; - // 1-based - highlight?: SourceHighlight[]; - revealLine?: number; -} - -export const Source: React.FC = ({ - text, - language, - highlight = [], - revealLine -}) => { - return ; -}; diff --git a/tests/library/inspector/pause.spec.ts b/tests/library/inspector/pause.spec.ts index a82d10e6ae..d6c2f2d258 100644 --- a/tests/library/inspector/pause.spec.ts +++ b/tests/library/inspector/pause.spec.ts @@ -294,7 +294,7 @@ it.describe('pause', () => { })().catch(e => e); const recorderPage = await recorderPageGetter(); await recorderPage.click('[title="Resume (F8)"]'); - await recorderPage.waitForSelector('.source-line-error'); + await recorderPage.waitForSelector('.source-line-error-underline'); expect(await sanitizeLog(recorderPage)).toEqual([ 'page.pause- XXms', 'page.getByRole(\'button\').isChecked()- XXms',