Skip to content
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
34 changes: 34 additions & 0 deletions tests/spec/features/highlighting_mir_output_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require 'spec_helper'
require 'support/editor'
require 'support/playground_actions'

RSpec.feature "Highlighting MIR output", type: :feature, js: true do
include PlaygroundActions

before do
visit '/'
editor.set(code)
in_build_menu { click_on("MIR") }
end

scenario "error locations are links" do
within('.output-mir') do
click_link('src/main.rs:4:14: 4:19', match: :first)
end
expect(editor).to have_highlighted_text('a + b')
end

def editor
Editor.new(page)
end

def code
<<~EOF
fn main() {
let a = 1;
let b = 2;
let _c = a + b;
}
EOF
end
end
13 changes: 13 additions & 0 deletions tests/spec/support/editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,17 @@ def set(text)
def has_line?(text)
page.has_css? '.ace_line', text: text
end

def has_highlighted_text?(text)
page.within('.editor .ace_text-input', visible: :any) do
selected = page.evaluate_script <<~JS
(() => {
const editor = document.querySelector('.ace_editor').env.editor;
return editor.getSelectedText();
})()
JS

selected == text
end
end
end
36 changes: 27 additions & 9 deletions ui/frontend/AdvancedEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { connect } from 'react-redux';

import State from './state';
import { CommonEditorProps, Crate, Edition, Focus, PairCharacters } from './types';
import { CommonEditorProps, Crate, Edition, Focus, PairCharacters, Position, Selection } from './types';

type Ace = typeof import('ace-builds');
type AceEditor = import('ace-builds').Ace.Editor;
Expand Down Expand Up @@ -63,10 +63,8 @@ interface AdvancedEditorProps {
execute: () => any;
keybinding?: string;
onEditCode: (_: string) => any;
position: {
line: number;
column: number;
};
position: Position;
selection: Selection;
theme: string;
crates: Crate[];
focus?: Focus;
Expand Down Expand Up @@ -253,6 +251,28 @@ const AdvancedEditor: React.SFC<AdvancedEditorProps> = props => {
editor.focus();
}, []));

const selectionProps = useMemo(() => ({
selection: props.selection,
ace: props.ace,
}), [props.selection, props.ace]);

useEditorProp(editor, selectionProps, useCallback((editor, { ace, selection }) => {
if (selection.start && selection.end) {
// Columns are zero-indexed in ACE, but why does the selection
// API and `gotoLine` treat the row/line differently?
const toPoint = ({ line, column }: Position) => ({ row: line - 1, column: column - 1 });

const start = toPoint(selection.start);
const end = toPoint(selection.end);

const range = new ace.Range(start.row, start.column, end.row, end.column);

editor.selection.setRange(range);
editor.renderer.scrollCursorIntoView(start);
editor.focus();
}
}, []));

// There's a tricky bug with Ace:
//
// 1. Open the page
Expand Down Expand Up @@ -297,10 +317,8 @@ interface AdvancedEditorAsyncProps {
execute: () => any;
keybinding?: string;
onEditCode: (_: string) => any;
position: {
line: number;
column: number;
};
position: Position;
selection: Selection;
theme: string;
crates: Crate[];
focus?: Focus;
Expand Down
71 changes: 59 additions & 12 deletions ui/frontend/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,49 @@ import { useSelector, useDispatch } from 'react-redux';

import * as actions from './actions';
import AdvancedEditor from './AdvancedEditor';
import { CommonEditorProps, Editor as EditorType } from './types';
import { CommonEditorProps, Editor as EditorType, Position, Selection } from './types';
import { State } from './reducers';

class CodeByteOffsets {
readonly code: string;
readonly lines: string[];

constructor(code: string) {
this.code = code;
this.lines = code.split('\n');
}

public lineToOffsets(line: number) {
const precedingBytes = this.bytesBeforeLine(line);

const highlightedLine = this.lines[line];
const highlightedBytes = highlightedLine.length;

return [precedingBytes, precedingBytes + highlightedBytes];
}

public rangeToOffsets(start: Position, end: Position) {
const startBytes = this.positionToBytes(start);
const endBytes = this.positionToBytes(end);
return [startBytes, endBytes];
}

private positionToBytes(position: Position) {
// Subtract one as this logic is zero-based and the columns are one-based
return this.bytesBeforeLine(position.line) + position.column - 1;
}

private bytesBeforeLine(line: number) {
// Subtract one as this logic is zero-based and the lines are one-based
line -= 1;

const precedingLines = this.lines.slice(0, line);

// Add one to account for the newline we split on and removed
return precedingLines.map(l => l.length + 1).reduce((a, b) => a + b);
}
}

class SimpleEditor extends React.PureComponent<CommonEditorProps> {
private _editor: HTMLTextAreaElement;

Expand Down Expand Up @@ -35,35 +75,41 @@ class SimpleEditor extends React.PureComponent<CommonEditorProps> {

public componentDidUpdate(prevProps, _prevState) {
this.gotoPosition(prevProps.position, this.props.position);
this.setSelection(prevProps.selection, this.props.selection);
}

private gotoPosition(oldPosition, newPosition) {
private gotoPosition(oldPosition: Position, newPosition: Position) {
const editor = this._editor;

if (!newPosition || !editor) { return; }
if (newPosition === oldPosition) { return; }

// Subtract one as this logix is zero-based and the lines are one-based
const line = newPosition.line - 1;
const { code } = this.props;
const offsets = new CodeByteOffsets(this.props.code);
const [startBytes, endBytes] = offsets.lineToOffsets(newPosition.line);

const lines = code.split('\n');
editor.focus();
editor.setSelectionRange(startBytes, endBytes);
}

const precedingLines = lines.slice(0, line);
const highlightedLine = lines[line];
private setSelection(oldSelection: Selection, newSelection: Selection) {
const editor = this._editor;

// Add one to account for the newline we split on and removed
const precedingBytes = precedingLines.map(l => l.length + 1).reduce((a, b) => a + b);
const highlightedBytes = highlightedLine.length;
if (!newSelection || !editor) { return; }
if (newSelection === oldSelection) { return; }

const offsets = new CodeByteOffsets(this.props.code);
const [startBytes, endBytes] = offsets.rangeToOffsets(newSelection.start, newSelection.end);

editor.setSelectionRange(precedingBytes, precedingBytes + highlightedBytes);
editor.focus();
editor.setSelectionRange(startBytes, endBytes);
}
}

const Editor: React.SFC = () => {
const code = useSelector((state: State) => state.code);
const editor = useSelector((state: State) => state.configuration.editor);
const position = useSelector((state: State) => state.position);
const selection = useSelector((state: State) => state.selection);
const crates = useSelector((state: State) => state.crates);

const dispatch = useDispatch();
Expand All @@ -76,6 +122,7 @@ const Editor: React.SFC = () => {
<div className="editor">
<SelectedEditor code={code}
position={position}
selection={selection}
crates={crates}
onEditCode={onEditCode}
execute={execute} />
Expand Down
3 changes: 2 additions & 1 deletion ui/frontend/Output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Execute from './Output/Execute';
import Gist from './Output/Gist';
import Section from './Output/Section';
import SimplePane, { SimplePaneProps } from './Output/SimplePane';
import PaneWithMir from './Output/PaneWithMir';
import * as selectors from './selectors';

const Tab: React.SFC<TabProps> = ({ kind, focus, label, onClick, tabProps }) => {
Expand Down Expand Up @@ -82,7 +83,7 @@ const Output: React.SFC = () => {
{focus === Focus.MacroExpansion && <SimplePane {...macroExpansion} kind="macro-expansion" />}
{focus === Focus.Asm && <PaneWithCode {...assembly} kind="asm" />}
{focus === Focus.LlvmIr && <PaneWithCode {...llvmIr} kind="llvm-ir" />}
{focus === Focus.Mir && <PaneWithCode {...mir} kind="mir" />}
{focus === Focus.Mir && <PaneWithMir {...mir} kind="mir" />}
{focus === Focus.Wasm && <PaneWithCode {...wasm} kind="wasm" />}
{focus === Focus.Gist && <Gist />}
</div>
Expand Down
24 changes: 24 additions & 0 deletions ui/frontend/Output/PaneWithMir.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { PrismCode } from 'react-prism';

import Header from './Header';
import SimplePane, { SimplePaneProps } from './SimplePane';

interface PaneWithMirProps extends SimplePaneProps {
code?: string;
}

const PaneWithMir: React.SFC<PaneWithMirProps> = ({ code, ...rest }) => (
<SimplePane {...rest}>
<div className="output-result">
<Header label="Result" />
<pre>
<PrismCode className="language-rust_mir">
{code}
</PrismCode>
</pre>
</div>
</SimplePane>
);

export default PaneWithMir;
11 changes: 9 additions & 2 deletions ui/frontend/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
PrimaryActionAuto,
PrimaryActionCore,
ProcessAssembly,
Position,
makePosition,
} from './types';

const routes = {
Expand Down Expand Up @@ -92,6 +94,7 @@ export enum ActionType {
AddImport = 'ADD_IMPORT',
EnableFeatureGate = 'ENABLE_FEATURE_GATE',
GotoPosition = 'GOTO_POSITION',
SelectText = 'SELECT_TEXT',
RequestFormat = 'REQUEST_FORMAT',
FormatSucceeded = 'FORMAT_SUCCEEDED',
FormatFailed = 'FORMAT_FAILED',
Expand Down Expand Up @@ -429,8 +432,11 @@ export const addImport = (code: string) =>
export const enableFeatureGate = (featureGate: string) =>
createAction(ActionType.EnableFeatureGate, { featureGate });

export const gotoPosition = (line, column) =>
createAction(ActionType.GotoPosition, { line: +line, column: +column });
export const gotoPosition = (line: string | number, column: string | number) =>
createAction(ActionType.GotoPosition, makePosition(line, column));

export const selectText = (start: Position, end: Position) =>
createAction(ActionType.SelectText, { start, end });

const requestFormat = () =>
createAction(ActionType.RequestFormat);
Expand Down Expand Up @@ -792,6 +798,7 @@ export type Action =
| ReturnType<typeof addImport>
| ReturnType<typeof enableFeatureGate>
| ReturnType<typeof gotoPosition>
| ReturnType<typeof selectText>
| ReturnType<typeof requestFormat>
| ReturnType<typeof receiveFormatSuccess>
| ReturnType<typeof receiveFormatFailure>
Expand Down
28 changes: 28 additions & 0 deletions ui/frontend/highlighting.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Prism from 'prismjs';
import { makePosition } from './types';

export function configureRustErrors({
enableFeatureGate,
getChannel,
gotoPosition,
selectText,
addImport,
reExecuteWithBacktrace,
}) {
Expand Down Expand Up @@ -48,6 +50,10 @@ export function configureRustErrors({
'backtrace-enable': /Run with `RUST_BACKTRACE=1` environment variable to display a backtrace/i,
};

Prism.languages.rust_mir = { // eslint-disable-line @typescript-eslint/camelcase
'mir-source': /src\/.*.rs:\d+:\d+: \d+:\d+/,
}

Prism.hooks.add('wrap', env => {
if (env.type === 'error-explanation') {
const errorMatch = /E\d+/.exec(env.content);
Expand Down Expand Up @@ -104,6 +110,16 @@ export function configureRustErrors({
env.attributes['data-line'] = line;
env.attributes['data-col'] = '1';
}
if (env.type === 'mir-source') {
const lineMatch = /(\d+):(\d+): (\d+):(\d+)/.exec(env.content);
const [_, startLine, startCol, endLine, endCol] = lineMatch;
env.tag = 'a';
env.attributes.href = '#';
env.attributes['data-start-line'] = startLine;
env.attributes['data-start-col'] = startCol;
env.attributes['data-end-line'] = endLine;
env.attributes['data-end-col'] = endCol;
}
});

Prism.hooks.add('after-highlight', env => {
Expand Down Expand Up @@ -141,5 +157,17 @@ export function configureRustErrors({
reExecuteWithBacktrace();
};
});

const mirSourceLinks = env.element.querySelectorAll('.mir-source');
Array.from(mirSourceLinks).forEach((link: HTMLAnchorElement) => {
const { startLine, startCol, endLine, endCol } = link.dataset;
const start = makePosition(startLine, startCol);
const end = makePosition(endLine, endCol);

link.onclick = e => {
e.preventDefault();
selectText(start, end);
};
});
});
}
2 changes: 2 additions & 0 deletions ui/frontend/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
editCode,
enableFeatureGate,
gotoPosition,
selectText,
addImport,
performCratesLoad,
performVersionsLoad,
Expand Down Expand Up @@ -49,6 +50,7 @@ const store = createStore(playgroundApp, initialState, enhancers);
configureRustErrors({
enableFeatureGate: featureGate => store.dispatch(enableFeatureGate(featureGate)),
gotoPosition: (line, col) => store.dispatch(gotoPosition(line, col)),
selectText: (start, end) => store.dispatch(selectText(start, end)),
addImport: (code) => store.dispatch(addImport(code)),
reExecuteWithBacktrace: () => store.dispatch(reExecuteWithBacktrace()),
getChannel: () => store.getState().configuration.channel,
Expand Down
2 changes: 2 additions & 0 deletions ui/frontend/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import notifications from './notifications';
import output from './output';
import page from './page';
import position from './position';
import selection from './selection';
import versions from './versions';

const playgroundApp = combineReducers({
Expand All @@ -19,6 +20,7 @@ const playgroundApp = combineReducers({
output,
page,
position,
selection,
versions,
});

Expand Down
Loading