Skip to content

Commit

Permalink
Merge pull request #1 from mmuzikar/complete-refactor
Browse files Browse the repository at this point in the history
Complete refactor
  • Loading branch information
mmuzikar authored Feb 17, 2020
2 parents e7dc1ec + eb6e7bb commit 29f0380
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 62 deletions.
17 changes: 17 additions & 0 deletions .project
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>bytecodemanipulation</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>
2 changes: 2 additions & 0 deletions docs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Ideas for thesis:
*
82 changes: 46 additions & 36 deletions ui/src/components/CompletePrompt.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,41 @@
import React, { Component } from "react";
import Fuse, { FuseOptions } from "fuse.js";
import { Step } from "../interop/cucumberTypes";
import { StepManager } from "../interop/stepManager";
import { keyDispatcher } from "./TerminalInput";

type Props = {
type Props<T> = {
value: string,
show: boolean,
keys?: {name: string, weight: number}[]
getData: () => Promise<T[]> | undefined,
toggleSending: (arg0:boolean) => void,
fillIn: (arg0:Step) => void
fillIn: (arg0:T) => void,
render: (arg0:T) => JSX.Element
};
type State = {
steps: Step[],
type State<T> = {
data: T[],
loading: boolean,
currentIndex: number
};

export class CompletePrompt extends Component<Props, State> {
export class CompletePrompt<T> extends Component<Props<T>, State<T>> {

static fuseOptions : FuseOptions<Step> = {
fuseOptions : FuseOptions<T> = {
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
{
name: "pattern",
weight: 0.7
}, {
name: "docs",
weight: 0.3
}
]
keys: this.props.keys
}
state = {
steps: [],
loading: false,
data: [],
currentIndex: -1
}

componentDidMount(){
const outOfRange = () => this.state.currentIndex === -1 || this.state.currentIndex === this.state.steps.length;
const outOfRange = () => this.state.currentIndex === -1 || this.state.currentIndex === this.state.data.length;
keyDispatcher.register((event) => {
if (!this.props.show){
this.props.toggleSending(true);
Expand All @@ -53,13 +48,13 @@ export class CompletePrompt extends Component<Props, State> {
});
} else {
this.setState((prevState) => ({
currentIndex: Math.min(prevState.currentIndex+1, prevState.steps.length)
currentIndex: Math.min(prevState.currentIndex+1, prevState.data.length)
}))
}
} else if (event.key === "ArrowUp"){
if (outOfRange()){
this.setState({
currentIndex: this.state.steps.length - 1
currentIndex: this.state.data.length - 1
})
} else {
this.setState((prevState) => ({
Expand All @@ -68,36 +63,50 @@ export class CompletePrompt extends Component<Props, State> {
}
} else if (event.key === "Enter"){
if (!outOfRange()){
this.props.fillIn((this.state.steps[this.state.currentIndex] as Step));
this.props.fillIn((this.state.data[this.state.currentIndex] as T));
}
}
this.props.toggleSending(outOfRange());
})
this.updateValues();
}

componentDidUpdate(prevProps:Props){
if (this.props.value !== prevProps.value){
StepManager.get().getSteps().then((steps) => {
if (steps){
const fuse = new Fuse(steps, CompletePrompt.fuseOptions);
updateValues(){
const val = this.props.getData();
if (val){
this.setState({loading: true});
val.then(val => {
if (val){
const fuse = new Fuse(val, this.fuseOptions);
const results = fuse.search(this.props.value);
this.setState({
steps: results as Step[],
currentIndex: -1
data: results as T[],
currentIndex: -1,
loading: false
});
}
})
}
}

componentDidUpdate(prevProps:Props<T>, prevState:State<T>){
if (this.props.value !== prevProps.value){
console.debug("Prop value changed!");
this.updateValues();
}
}

render(){
const offset = (this.state.steps.length * 20);
const offset = (this.state.data.length * 20);
if (!this.props.show){
return <></>;
}
if (this.state.loading){
return <div className="complete-prompt" style={{top: -offset}}>Loading...</div>
}
return <ul className="complete-prompt" style={{top: -offset}}>
{(this.state.steps||[]).map((val:Step, i) =>
<CompleteEntry setActive={(active) => {
{(this.state.data||[]).map((val:T, i) =>
<CompleteEntry<T> setActive={(active) => {
if (active){
this.setState({
currentIndex: i
Expand All @@ -108,22 +117,23 @@ export class CompletePrompt extends Component<Props, State> {
})
}
}} active={i === this.state.currentIndex}
step={val}
value={val}
key={`complete_${i}`}
fillIn={() => this.props.fillIn((this.state.steps[this.state.currentIndex] as Step))}
render={this.props.render}
fillIn={() => this.props.fillIn((this.state.data[this.state.currentIndex] as T))}
/>)}
</ul>;
}
}

class CompleteEntry extends Component<{active: boolean, step:Step, setActive: (arg0:boolean) => void, fillIn: () => void}> {
class CompleteEntry<T> extends Component<{active: boolean, render: (arg0:T) => JSX.Element, value:T, setActive: (arg0:boolean) => void, fillIn: () => void}> {

render(){
return <li className={this.props.active?"complete-prompt-entry-active":""}
onMouseEnter={() => this.props.setActive(true)}
onMouseLeave={() => this.props.setActive(false)}
onClick={this.props.fillIn}>
{this.props.step.pattern}
{this.props.render(this.props.value)}
</li>
}

Expand Down
125 changes: 99 additions & 26 deletions ui/src/components/TerminalInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type State = {
val:string,
canSend:boolean,
stepRef?: Step,
parsedInput: (string | number)[]
};

export class TerminalInput extends Component<{}, State> {
Expand All @@ -26,10 +27,25 @@ export class TerminalInput extends Component<{}, State> {
val: "",
canSend: true,
stepRef: undefined,
parsedInput: []
}

handleSubmit(input:HTMLInputElement){
StepManager.get().runStep(input.value);
handleSubmit(target:HTMLElement){
console.debug(target);
let stepBuild = '';
for (let i = 0; i < target.children.length; i++){
const item = target.children.item(i);
if (item instanceof HTMLInputElement){
stepBuild += item.value;
} else {
stepBuild += (item as HTMLElement).textContent;
}
}
StepManager.get().runStep(stepBuild);
this.setState({
parsedInput: [],
stepRef: undefined
});
}

getStepRef(){
Expand All @@ -41,29 +57,38 @@ export class TerminalInput extends Component<{}, State> {
switch(input.key){
case "Enter":
input.preventDefault();
if (this.state.stepRef !== undefined && this.state.val.length > 0){
this.handleSubmit(target);
//TODO: if user is pressing Enter repeatedly tell them they need to use Shift
} else if (input.shiftKey){
this.handleSubmit(target);
if (input.shiftKey){
this.handleSubmit(document.getElementById("terminal-input")!);
}
// if (this.state.stepRef !== undefined && this.state.val.length > 0){
// this.handleSubmit(document.getElementById("terminal-input")!);
// //TODO: if user is pressing Enter repeatedly tell them they need to use Shift
// } else if (input.shiftKey){
// this.handleSubmit(target);
// }
break;
case "Tab":
input.preventDefault();
if (this.state.stepRef && this.state.stepRef !== undefined && this.getStepRef().args){
let offset = this.state.val.length - this.getStepRef().pattern.length;
//TODO: ugh
let i = 0;
const args = this.getStepRef().args as Argument[];
while(target.selectionStart! < args[i].start!){
i++;
if (i > args.length){
i = 0;
break;
if (this.state.parsedInput.length > 0){
input.preventDefault();
const active = document.activeElement;
if (active && active.classList.contains('arg')){
const id = Number.parseInt(active.getAttribute('tabIndex')!);
const inputs = document.getElementsByClassName('arg');
if (id + 1 >= inputs.length){
(inputs.item(0)! as HTMLElement).focus();
} else {
(inputs.item(id + 1)! as HTMLElement).focus();
}
}
}
break;
break;
case "Escape":
input.preventDefault();
this.setState({
parsedInput: [],
stepRef: undefined
});
break;
default:

break;
Expand All @@ -84,25 +109,73 @@ export class TerminalInput extends Component<{}, State> {
}

onSetStep(step: Step){
this.setState({val: step ? step.pattern : "", stepRef: step}, () => {
let split = [];
if (step && step.args){
split.push(step.pattern.substring(0, step.args[0].start!));
split.push(0);
for (let i = 1; i < step.args.length; i++){
split.push(step.pattern.substring(step.args[i-1].end! + 1, step.args[i].start!));
split.push(i);
}
split.push(step.pattern.substring(step.args[step.args.length - 1].end! + 1));
}
this.setState({val: step ? step.pattern : "", parsedInput: split, stepRef: step}, () => {
if (step && step.args){
const input = document.getElementById("terminal-input") as HTMLInputElement;
if (input){
input.focus();
input.setSelectionRange(step.args[0].start!, step.args[0].end! + 1);
const firstInput = document.getElementById('arg-0') as HTMLInputElement;
if (firstInput){
firstInput.focus();
}
}
});
}

render(){
let val = this.state.parsedInput.length > 0 && false ? this.state.parsedInput.join("") : this.state.val;
let input = <input autoFocus id="terminal-input" style={{width: "85%"}} value={val} onChange={this.handleChange} onKeyDownCapture={this.handleInput}/>;
if (this.state.parsedInput.length > 0){
const focusedInput = document.activeElement as HTMLInputElement;
input = <>
<CompletePrompt<{val: string}> value={focusedInput.value}
getData={() =>StepManager.get().getSuggestionForArg(this.state.stepRef!, focusedInput.tabIndex)}
fillIn={(val) => focusedInput.value = val.val}
render={(arg) => <span>{arg.val}</span>}
show={focusedInput !== undefined}
toggleSending={(arg) => this.setState({canSend: arg})}
keys={[{
name: "val",
weight: 1
}]}
/>
<span id="terminal-input" onKeyDownCapture={this.handleInput}>
{
this.state.parsedInput.map((val, i) =>
typeof(val) === "string" ?
<span key={`const_${i}`}>{val}</span> :
<input key={`var_${val}`} className='arg' id={`arg-${val}`}
autoFocus={val === 0} tabIndex={val} onChange={() => this.forceUpdate()}
/>
)
}
</span></>;
}
return <div style={{width: "100% "}}>
<CompletePrompt value={this.state.val}
<CompletePrompt<Step> value={this.state.val}
toggleSending={(arg) => this.setState({canSend: arg})}
show={this.state.stepRef === undefined}
fillIn={this.onSetStep}
getData={() => StepManager.get().getSteps()}
render={(step) => <span>{step.pattern}</span>}
keys={[
{
name: "pattern",
weight: 0.7
}, {
name: "docs",
weight: 0.3
}
]}
/>
<input id="terminal-input" style={{width: "85%"}} value={this.state.val} onChange={this.handleChange} onKeyDownCapture={this.handleInput}/>
{input}
<input type="submit"/>
</div>
}
Expand Down
30 changes: 30 additions & 0 deletions ui/src/interop/stepManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@ export class StepManager {
})
}

getSuggestionForArg(step:Step, i:number, stepArgs:string[] = []):Promise<{val: string}[]> | undefined{
if (step.args){
const args = step.args;

const arg = args[i];
if (arg.suggProvider !== ""){
return new Promise((resolve) => {
if (i < 0 || i >= args.length){
resolve([]);
}
else {
fetch(`${AppConfig.getServerUrl()}/suggestion`, {
method: "POST",
body: JSON.stringify({
step: step.pattern,
args: stepArgs,
argId: i
})
}).then((r => r.json())).then((suggs:string[]) => {
console.log(suggs.map((v) => ({val: v})));
resolve(suggs.map((v) => ({val: v})))
})
}
});
}
} else {
return undefined;
}
}

fetchSteps(callback:(value?:Step[]) => void | undefined){
fetch(`${AppConfig.getServerUrl()}/liststeps`).then((r) => r.json()).then((steps:Step[]) => {
this.stepRepo = this.analyzeParams(steps);
Expand Down

0 comments on commit 29f0380

Please sign in to comment.