Moved "extension/ui" to "webview", removed "shared".

This commit is contained in:
Henning Dieterichs
2020-02-29 21:31:22 +01:00
parent f55bef23c2
commit 5318b5e4ae
56 changed files with 144 additions and 116 deletions

View File

@@ -0,0 +1,108 @@
import React = require("react");
import { Model } from "../model/Model";
import { observer, disposeOnUnmount } from "mobx-react";
import { observable, action, autorun } from "mobx";
import * as monaco from "monaco-editor";
@observer
export class ExpressionInput extends React.Component<{ model: Model }> {
@observable private editor: monaco.editor.IStandaloneCodeEditor | undefined;
@observable private contentHeight: number | undefined = undefined;
private model = monaco.editor.createModel(
"",
"javascript",
monaco.Uri.parse(`file:///main.ts`)
);
render() {
return (
<div
className="component-monaco-editor"
style={{
// The monaco editor does not have a padding, so we add our own.
// We have to match the colors of monaco's background.
backgroundColor:
this.props.model.theme === "light"
? "white"
: "#263238",
}}
>
<div
className="part-editor"
style={{
height: this.contentHeight,
}}
ref={this.setEditorDiv}
/>
</div>
);
}
componentWillUnmount() {
if (this.editor) {
this.editor.dispose();
}
this.model.dispose();
}
@disposeOnUnmount
private _updateText = autorun(() => {
const val = this.model.getValue();
const newVal = this.props.model.expression;
if (val !== newVal) {
this.model.setValue(newVal);
}
});
private readonly setEditorDiv = (editorDiv: HTMLDivElement) => {
if (!editorDiv) {
return;
}
this.editor = monaco.editor.create(editorDiv, {
model: this.model,
scrollBeyondLastLine: false,
minimap: { enabled: false },
fixedOverflowWidgets: true,
lineNumbers: "off",
glyphMargin: false,
folding: false,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
automaticLayout: true,
scrollbar: {
horizontal: "hidden",
vertical: "hidden",
horizontalScrollbarSize: 0,
verticalScrollbarSize: 0,
},
/*theme: { dark: "vs-dark", light: "vs-light" }[
this.props.model.theme
],*/
});
//
this.editor.onDidContentSizeChange(e => {
this.contentHeight = e.contentHeight;
});
this.editor.onKeyDown(e => {
if (e.keyCode == monaco.KeyCode.Enter) {
if (
(this.model.getLineCount() <= 1 || e.ctrlKey) &&
!e.shiftKey
) {
e.preventDefault();
e.stopPropagation();
this.submit();
}
}
});
};
@action.bound
submit() {
const val = this.model.getValue();
console.log(val);
this.props.model.setExpression(val);
}
}

View File

@@ -0,0 +1,138 @@
import React = require("react");
import { Button, Spinner } from "@blueprintjs/core";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import { Model } from "../model/Model";
import { ExpressionInput } from "./ExpressionInput";
import { Visualizer } from "./Visualizer";
import classnames = require("classnames");
import { VisualizerHeaderDetails } from "./VisualizerHeaderDetails";
@observer
export class GUI extends React.Component<{ model: Model }> {
render() {
const m = this.props.model;
return (
<div
className="component-GUI"
tabIndex={0}
style={{
display: "flex",
flexDirection: "column",
height: "100%",
}}
>
<div className="part-Header">
<VisualizerHeader model={m} />
</div>
<div
className="part-Visualizer"
style={{ flex: 1, minHeight: 0 }}
>
<Visualizer model={m} />
</div>
</div>
);
}
}
@observer
export class VisualizerHeader extends React.Component<{ model: Model }> {
@observable expanded = false;
render() {
const m = this.props.model;
return (
<div
className="component-VisualizerHeader"
style={{ display: "flex", flexDirection: "row" }}
>
<div
className={classnames(
"part-ExpandButton",
this.expanded && "expanded"
)}
onClick={this.toggleExpanded}
/>
<div
className="part-HeaderContent"
style={{
flex: 1,
minWidth: 0,
display: "flex",
flexDirection: "column",
}}
>
<div className="part-HeaderMain">
<VisualizerHeaderMain model={m} />
</div>
{this.expanded && (
<>
<div style={{ height: 6 }} />
<VisualizerHeaderDetails model={m} />
</>
)}
</div>
</div>
);
}
@action.bound
toggleExpanded() {
this.expanded = !this.expanded;
}
}
export class VisualizerHeaderMain extends React.Component<{ model: Model }> {
render() {
const m = this.props.model;
return (
<div
className="component-VisualizerHeaderMain"
style={{
display: "flex",
alignItems: "center",
}}
>
<div
className="part-ExpressionInput "
style={{ flex: 1, minWidth: 0 }}
>
<ExpressionInput model={m} />
</div>
<div style={{ width: 4 }} />
{m.loading ? (
<div style={{ padding: "0 4px" }}>
<Spinner size={Spinner.SIZE_SMALL} />
</div>
) : (
<Button
minimal
small
className="part-Icon"
icon="refresh"
onClick={() => m.refresh()}
/>
)}
<Button
minimal
small
className="part-Icon"
icon="log-in"
onClick={() => m.openBrowser()}
/>
{/*
TODO
<Button
minimal
small
className="part-Icon"
icon="locate"
onClick={() => m.useSelectionAsExpression()}
/>
*/}
</div>
);
}
}

View File

@@ -0,0 +1,47 @@
import React = require("react");
import { observer } from "mobx-react";
import { observable } from "mobx";
import Measure from "react-measure";
@observer
export class NoData extends React.Component<{ label: string }> {
@observable height = 0;
@observable width = 0;
render() {
const { width, height } = this;
return (
<Measure
client={true}
onResize={e => {
if (e.client) {
this.height = e.client.height;
this.width = e.client.width;
}
}}
>
{({ measureRef }) => (
<div className="component-NoData" ref={measureRef}>
<svg>
<line x1={0} y1={0} x2={width} y2={height} />
<line x1={width} y1={0} x2={0} y2={height} />
<rect
x={width / 2 - 50}
y={height / 2 - 20}
width={100}
height={40}
/>
<text
x={width / 2}
y={height / 2}
textAnchor="middle"
alignmentBaseline="central"
children={this.props.label}
/>
</svg>
</div>
)}
</Measure>
);
}
}

View File

@@ -0,0 +1,256 @@
function sqr(a: number) {
return a * a;
}
export type PointLike =
| Point
| { x?: number; y: number }
| { x: number; y?: number };
export function point(data: PointLike) {
if (data instanceof Point) {
return data;
}
return new Point(data.x || 0, data.y || 0);
}
export class Point {
public static readonly Zero = new Point(0, 0);
constructor(public readonly x: number, public readonly y: number) {}
public mapXY(fn: (arg: number) => number): Point {
return new Point(fn(this.x), fn(this.y));
}
public distance(other: PointLike = Point.Zero): number {
const d = this.sub(other);
return Math.sqrt(sqr(d.x) + sqr(d.y));
}
public sub(other: PointLike): Point {
const o = point(other);
return new Point(this.x - o.x, this.y - o.y);
}
public add(other: PointLike): Point {
const o = point(other);
return new Point(this.x + o.x, this.y + o.y);
}
public mul(scalar: number): Point {
return new Point(this.x * scalar, this.y * scalar);
}
public div(scalar: number): Point {
return new Point(this.x / scalar, this.y / scalar);
}
public equals(other: PointLike) {
const o = point(other);
return this.x === o.x && this.y === o.y;
}
public angle(): number {
const angle = Math.atan2(this.x, this.y);
return angle;
}
public getPointCloserTo(dest: PointLike, dist: number): Point {
if (this.equals(dest)) return this;
const angle = point(dest)
.sub(this)
.angle();
const result = new Point(
this.x + Math.sin(angle) * dist,
this.y + Math.cos(angle) * dist
);
return result;
}
public toJson(): { x: number; y: number } {
return { x: this.x, y: this.y };
}
}
function turn(p1: Point, p2: Point, p3: Point): number {
const A = (p3.y - p1.y) * (p2.x - p1.x);
const B = (p2.y - p1.y) * (p3.x - p1.x);
return A > B + Number.MIN_VALUE ? 1 : A + Number.MIN_VALUE < B ? -1 : 0;
}
export function isIntersect(
aStart: Point,
aEnd: Point,
bStart: Point,
bEnd: Point
): boolean {
return (
turn(aStart, bStart, bEnd) != turn(aEnd, bStart, bEnd) &&
turn(aStart, aEnd, bStart) != turn(aStart, aEnd, bEnd)
);
}
/**
* Liang-Barsky function by Daniel White
*
* @link http://www.skytopia.com/project/articles/compsci/clipping.html
*
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
* @param {array<number>} bbox
* @return {array<array<number>>|null}
*/
function liangBarsky(
x0: number,
y0: number,
x1: number,
y1: number,
bbox: [number, number, number, number]
): [[number, number], [number, number]] | null {
let [xmin, xmax, ymin, ymax] = bbox;
let t0 = 0,
t1 = 1;
let dx = x1 - x0,
dy = y1 - y0;
let p = 0,
q = 0,
r = 0;
for (let edge: 0 | 1 | 2 | 3 = 0; edge < 4; edge++) {
// Traverse through left, right, bottom, top edges.
if (edge === 0) {
p = -dx;
q = -(xmin - x0);
} else if (edge === 1) {
p = dx;
q = xmax - x0;
} else if (edge === 2) {
p = -dy;
q = -(ymin - y0);
} else if (edge === 3) {
p = dy;
q = ymax - y0;
}
r = q / p;
if (p === 0 && q < 0) return null; // Don't draw line at all. (parallel line outside)
if (p < 0) {
if (r > t1) return null;
// Don't draw line at all.
else if (r > t0) t0 = r; // Line is clipped!
} else if (p > 0) {
if (r < t0) return null;
// Don't draw line at all.
else if (r < t1) t1 = r; // Line is clipped!
}
}
return [[x0 + t0 * dx, y0 + t0 * dy], [x0 + t1 * dx, y0 + t1 * dy]];
}
export function intersectRectWithLine(
start: Point,
end: Point,
rect: Rectangle
): { first: Point; second: Point } | undefined {
const r = liangBarsky(start.x, start.y, end.x, end.y, [
rect.topLeft.x,
rect.bottomRight.x,
rect.topLeft.y,
rect.bottomRight.y,
]);
if (!r) {
return undefined;
}
return {
first: new Point(r[0][0], r[0][1]),
second: new Point(r[1][0], r[1][1]),
};
}
// first zoom then translate
export function scale(
clientOffset: Point,
clientSize: Point,
viewSize: Point
): { clientZoom: number; clientOffset: Point } {
const clientRatio = clientSize.x / clientSize.y;
const viewRatio = viewSize.x / viewSize.y;
let zoom = 1;
if (clientRatio < viewRatio) zoom = viewSize.y / clientSize.y;
else zoom = viewSize.x / clientSize.x;
const clientMid = clientOffset.mul(zoom).add(clientSize.mul(zoom / 2));
const viewMid = viewSize.div(2);
const clientOffset2 = viewMid.sub(clientMid);
return { clientOffset: clientOffset2, clientZoom: zoom };
}
export class Rectangle {
public static ofSize(position: PointLike, size: PointLike): Rectangle {
const pos = point(position);
return new Rectangle(pos, pos.add(size));
}
public static spanning(first: Point, ...rest: Point[]): Rectangle {
let min = first.toJson();
let max = first.toJson();
for (const p of rest) {
min.x = Math.min(min.x, p.x);
min.y = Math.min(min.y, p.y);
max.x = Math.max(max.x, p.x);
max.y = Math.max(max.y, p.y);
}
return new Rectangle(point(min), point(max));
}
constructor(
public readonly topLeft: Point,
public readonly bottomRight: Point
) {}
get center(): Point {
return this.bottomRight.add(this.topLeft).div(2);
}
get size(): Point {
return this.bottomRight.sub(this.topLeft);
}
get topRight(): Point {
return new Point(this.bottomRight.x, this.topLeft.y);
}
get bottomLeft(): Point {
return new Point(this.topLeft.x, this.bottomRight.y);
}
public intersects(selectionRect: Rectangle): boolean {
return !(
selectionRect.topLeft.x > this.bottomRight.x ||
selectionRect.bottomRight.x < this.topLeft.x ||
selectionRect.topLeft.y > this.bottomRight.y ||
selectionRect.bottomRight.y < this.topLeft.y
);
}
equals(other: Rectangle) {
return (
this.topLeft.equals(other.topLeft) &&
this.bottomRight.equals(other.bottomRight)
);
}
}

View File

@@ -0,0 +1,90 @@
import { Point, Rectangle } from "./Point";
import * as React from "react";
import { Properties } from "csstype";
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
export function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
const newObj: Omit<T, K> = {} as any;
for (const [key, val] of Object.entries(obj)) {
if (!keys.includes(key as K)) {
(newObj as any)[key] = val;
}
}
return newObj;
}
export interface SvgAttributes<T> extends React.DOMAttributes<T> {
stroke?: string;
className?: string;
style?: Properties;
pointerEvents?: "none" | "auto" | "initial";
}
export function SvgText(
props: {
position: Point;
children: any;
childRef?: React.Ref<SVGTextElement>;
textAnchor?: "middle" | "end" | "start";
dominantBaseline?: "central" | "middle";
} & SvgAttributes<SVGTextElement>
) {
return (
<text
x={props.position.x}
y={props.position.y}
ref={props.childRef}
{...omit(props, ["position", "childRef"])}
/>
);
}
export function SvgCircle(
props: {
center: Point;
radius: number;
childRef?: React.Ref<SVGCircleElement>;
} & SvgAttributes<SVGCircleElement>
) {
return (
<circle
cx={props.center.x}
cy={props.center.y}
r={props.radius}
ref={props.childRef}
{...omit(props, ["center", "radius", "childRef"])}
/>
);
}
export function SvgLine(
props: { start: Point; end: Point } & SvgAttributes<SVGLineElement>
) {
return (
<line
x1={props.start.x}
y1={props.start.y}
x2={props.end.x}
y2={props.end.y}
{...omit(props, ["end", "start"])}
/>
);
}
export function SvgRect(
props: { rectangle: Rectangle; fill?: string } & SvgAttributes<
SVGRectElement
>
) {
const r = props.rectangle;
return (
<rect
x={r.topLeft.x}
y={r.topLeft.y}
width={r.size.x}
height={r.size.y}
{...omit(props, ["rectangle"])}
/>
);
}

View File

@@ -0,0 +1,36 @@
import React = require("react");
import { Model } from "../model/Model";
import { observer } from "mobx-react";
import { NoData } from "./NoData";
@observer
export class Visualizer extends React.Component<{ model: Model }> {
render() {
return (
<div className="component-Visualizer">{this.renderContent()}</div>
);
}
renderContent(): JSX.Element {
const s = this.props.model.state;
if (s.kind === "loading") {
return <NoData label="Loading" />;
} else if (s.kind === "error") {
return <NoData label={"Error: " + s.message} />;
} else if (s.kind === "noExpression") {
return <NoData label="No Expression" />;
} else if (s.kind === "noDebugSession") {
return <NoData label="No Debug Session" />;
} else if (s.kind === "data") {
const vis = this.props.model.visualizations;
if (!vis || !vis.visualization) {
return <NoData label="No Visualization" />;
}
return vis.visualization.render();
} else {
const nvr: never = s;
return <div />;
}
}
}

View File

@@ -0,0 +1,151 @@
import React = require("react");
import { Popover } from "@blueprintjs/core";
import { DataExtractorInfo } from "@hediet/debug-visualizer-data-extraction";
import { computed } from "mobx";
import { observer } from "mobx-react";
import { Model } from "../model/Model";
import { VisualizationId } from "../visualizers/Visualizer";
@observer
export class VisualizerHeaderDetails extends React.Component<{ model: Model }> {
render() {
const m = this.props.model;
let availableExtractors = new Array<DataExtractorInfo>();
let selectedExtractor = -1;
if (m.state.kind === "data") {
const result = m.state.result;
availableExtractors = result.availableExtractors;
selectedExtractor = result.availableExtractors.findIndex(
i => i.id === result.usedExtractor.id
);
}
let visualizations = new Array<{
label: string;
id: VisualizationId;
}>();
let selectedVisualization = -1;
if (m.visualizations) {
visualizations = m.visualizations.allVisualizations.map(v => ({
label: v.name,
id: v.id,
}));
if (m.visualizations.visualization) {
selectedVisualization = visualizations.findIndex(
v => v.id === m.visualizations!.visualization!.id
);
}
}
return (
<div
className="component-VisualizerHeaderDetails"
style={{ display: "flex", alignItems: "center" }}
>
{/*
<NamedSelect
name="Source"
selected={0}
options={[{ label: "js" }]}
onSelected={item => {}}
/>
*/}
<div style={{ width: 10 }} />
<NamedSelect
name="Extractor"
selected={selectedExtractor}
options={availableExtractors.map(e => ({
label: e.name,
id: e.id,
}))}
onSelected={item => {
m.setPreferredExtractorId(item.id);
}}
/>
<div style={{ width: 10 }} />
<NamedSelect
name="Visualizer"
selected={selectedVisualization}
onSelected={item => {
m.setPreferredVisualizationId(item.id);
}}
options={
m.visualizations
? m.visualizations.allVisualizations.map(v => ({
label: v.name,
id: v.id,
}))
: []
}
/>
</div>
);
}
}
function NamedSelect<T extends SelectableItem>(props: {
name: string;
selected: number;
onSelected: (item: T) => void;
options: T[];
}) {
return (
<div
className="component-NamedSelect"
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div className="part-Select" style={{ minWidth: 100 }}>
<Select {...props} />
</div>
<div className="part-Name">{props.name}</div>
</div>
);
}
interface SelectableItem {
label: string;
}
@observer
class Select<T extends SelectableItem> extends React.Component<{
selected: number;
onSelected: (item: T) => void;
options: T[];
}> {
@computed get selected(): T | undefined {
return this.props.options[this.props.selected];
}
render() {
return (
<div className="component-Select">
<Popover
position="auto-start"
modifiers={{ arrow: { enabled: false } }}
usePortal={false}
>
<button className="part-Button">
{this.selected ? this.selected.label : "(none)"}
</button>
<div className="part-Items">
{this.props.options.map((o, idx) => (
<div
key={idx}
className="part-Item"
onClick={() => this.props.onSelected(o)}
>
{o.label}
</div>
))}
</div>
</Popover>
</div>
);
}
}