Skip to content

Commit

Permalink
add auto-highlighting of selected/updated code
Browse files Browse the repository at this point in the history
  • Loading branch information
undergroundwires committed Aug 25, 2020
1 parent 5df4587 commit b789250
Show file tree
Hide file tree
Showing 39 changed files with 1,162 additions and 174 deletions.
26 changes: 17 additions & 9 deletions src/application/State/Code/ApplicationCode.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
import { CodeChangedEvent } from './Event/CodeChangedEvent';
import { CodePosition } from './Position/CodePosition';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
import { UserScriptGenerator } from './UserScriptGenerator';
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
import { Signal } from '@/infrastructure/Events/Signal';
import { IApplicationCode } from './IApplicationCode';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';

export class ApplicationCode implements IApplicationCode {
public readonly changed = new Signal<string>();
public readonly changed = new Signal<ICodeChangedEvent>();
public current: string;

private readonly generator: IUserScriptGenerator = new UserScriptGenerator();
private scriptPositions = new Map<SelectedScript, CodePosition>();

constructor(
userSelection: IUserSelection,
private readonly version: string) {
private readonly version: string,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
if (!version) { throw new Error('version is null or undefined'); }
this.generator = new UserScriptGenerator();
if (!generator) { throw new Error('generator is null or undefined'); }
this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => {
this.setCode(scripts);
});
}

private setCode(scripts: ReadonlyArray<SelectedScript>) {
this.current = scripts.length === 0 ? '' : this.generator.buildCode(scripts, this.version);
this.changed.notify(this.current);
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
const oldScripts = Array.from(this.scriptPositions.keys());
const code = this.generator.buildCode(scripts, this.version);
this.current = code.code;
this.scriptPositions = code.scriptPositions;
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
this.changed.notify(event);
}
}
64 changes: 64 additions & 0 deletions src/application/State/Code/Event/CodeChangedEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ICodeChangedEvent } from './ICodeChangedEvent';
import { SelectedScript } from '../../Selection/SelectedScript';
import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';

export class CodeChangedEvent implements ICodeChangedEvent {
public readonly code: string;
public readonly addedScripts: ReadonlyArray<IScript>;
public readonly removedScripts: ReadonlyArray<IScript>;
public readonly changedScripts: ReadonlyArray<IScript>;

private readonly scripts: Map<IScript, ICodePosition>;

constructor(
code: string,
oldScripts: ReadonlyArray<SelectedScript>,
scripts: Map<SelectedScript, ICodePosition>) {
ensureAllPositionsExist(code, Array.from(scripts.values()));
this.code = code;
const newScripts = Array.from(scripts.keys());
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
this.changedScripts = getChangedScripts(oldScripts, newScripts);
this.scripts = new Map<IScript, ICodePosition>();
scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position);
});
}

public isEmpty(): boolean {
return this.scripts.size === 0;
}

public getScriptPositionInCode(script: IScript): ICodePosition {
return this.scripts.get(script);
}
}

function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
const totalLines = script.split(/\r\n|\r|\n/).length;
for (const position of positions) {
if (position.endLine > totalLines) {
throw new Error(`script end line (${position.endLine}) is out of range.` +
`(total code lines: ${totalLines}`);
}
}
}

function getChangedScripts(
oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
return newScripts
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
&& oldScript.revert !== newScript.revert ))
.map((selection) => selection.script);
}

function selectIfNotExists(
selectableContainer: ReadonlyArray<SelectedScript>,
test: ReadonlyArray<SelectedScript>) {
return selectableContainer
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
.map((selection) => selection.script);
}
11 changes: 11 additions & 0 deletions src/application/State/Code/Event/ICodeChangedEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';

export interface ICodeChangedEvent {
readonly code: string;
addedScripts: ReadonlyArray<IScript>;
removedScripts: ReadonlyArray<IScript>;
changedScripts: ReadonlyArray<IScript>;
isEmpty(): boolean;
getScriptPositionInCode(script: IScript): ICodePosition;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@ const TotalFunctionSeparatorChars = 58;
export class CodeBuilder {
private readonly lines = new Array<string>();

// Returns current line starting from 0 (no lines), or 1 (have single line)
public get currentLine(): number {
return this.lines.length;
}

public appendLine(code?: string): CodeBuilder {
this.lines.push(code);
if (!code) {
this.lines.push('');
return this;
}
const lines = code.match(/[^\r\n]+/g);
for (const line of lines) {
this.lines.push(line);
}
return this;
}

Expand Down
7 changes: 7 additions & 0 deletions src/application/State/Code/Generation/IUserScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';

export interface IUserScript {
code: string;
scriptPositions: Map<SelectedScript, ICodePosition>;
}
6 changes: 4 additions & 2 deletions src/application/State/Code/Generation/IUserScriptGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';

import { IUserScript } from './IUserScript';
export interface IUserScriptGenerator {
buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string;
buildCode(
selectedScripts: ReadonlyArray<SelectedScript>,
version: string): IUserScript;
}
58 changes: 45 additions & 13 deletions src/application/State/Code/Generation/UserScriptGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { CodeBuilder } from './CodeBuilder';
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScript } from './IUserScript';

export const adminRightsScript = {
name: 'Ensure admin privileges',
Expand All @@ -13,22 +16,51 @@ export const adminRightsScript = {
};

export class UserScriptGenerator implements IUserScriptGenerator {
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string {
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): IUserScript {
if (!selectedScripts) { throw new Error('scripts is undefined'); }
if (!selectedScripts.length) { throw new Error('scripts are empty'); }
if (!version) { throw new Error('version is undefined'); }
const builder = new CodeBuilder()
.appendLine('@echo off')
.appendCommentLine(`https://privacy.sexy — v${version}${new Date().toUTCString()}`)
.appendFunction(adminRightsScript.name, adminRightsScript.code).appendLine();
let scriptPositions = new Map<SelectedScript, ICodePosition>();
if (!selectedScripts.length) {
return { code: '', scriptPositions };
}
const builder = initializeCode(version);
for (const selection of selectedScripts) {
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
const code = selection.revert ? selection.script.revertCode : selection.script.code;
builder.appendFunction(name, code).appendLine();
scriptPositions = appendSelection(selection, scriptPositions, builder);
}
return builder.appendLine()
.appendLine('pause')
.appendLine('exit /b 0')
.toString();
const code = finalizeCode(builder);
return { code, scriptPositions };
}
}

function initializeCode(version: string): CodeBuilder {
return new CodeBuilder()
.appendLine('@echo off')
.appendCommentLine(`https://privacy.sexy — v${version}${new Date().toUTCString()}`)
.appendFunction(adminRightsScript.name, adminRightsScript.code)
.appendLine();
}

function finalizeCode(builder: CodeBuilder): string {
return builder.appendLine()
.appendLine('pause')
.appendLine('exit /b 0')
.toString();
}

function appendSelection(
selection: SelectedScript,
scriptPositions: Map<SelectedScript, ICodePosition>,
builder: CodeBuilder): Map<SelectedScript, ICodePosition> {
const startPosition = builder.currentLine + 1;
appendCode(selection, builder);
const endPosition = builder.currentLine - 1;
builder.appendLine();
scriptPositions.set(selection, new CodePosition(startPosition, endPosition));
return scriptPositions;
}

function appendCode(selection: SelectedScript, builder: CodeBuilder) {
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
const scriptCode = selection.revert ? selection.script.revertCode : selection.script.code;
builder.appendFunction(name, scriptCode);
}
3 changes: 2 additions & 1 deletion src/application/State/Code/IApplicationCode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { ISignal } from '@/infrastructure/Events/ISignal';

export interface IApplicationCode {
readonly changed: ISignal<string>;
readonly changed: ISignal<ICodeChangedEvent>;
readonly current: string;
}
24 changes: 24 additions & 0 deletions src/application/State/Code/Position/CodePosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ICodePosition } from './ICodePosition';
export class CodePosition implements ICodePosition {

public get totalLines(): number {
return this.endLine - this.startLine;
}

constructor(
public readonly startLine: number,
public readonly endLine: number) {
if (startLine < 0) {
throw new Error('Code cannot start in a negative line');
}
if (endLine < 0) {
throw new Error('Code cannot end in a negative line');
}
if (endLine === startLine) {
throw new Error('Empty code');
}
if (endLine < startLine) {
throw new Error('End line cannot be less than start line');
}
}
}
5 changes: 5 additions & 0 deletions src/application/State/Code/Position/ICodePosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface ICodePosition {
readonly startLine: number;
readonly endLine: number;
readonly totalLines: number;
}
4 changes: 3 additions & 1 deletion src/application/State/Selection/IUserSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ export interface IUserSelection {
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>;
readonly totalSelected: number;
removeAllInCategory(categoryId: number): void;
addAllInCategory(categoryId: number): void;
addSelectedScript(scriptId: string, revert: boolean): void;
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
removeSelectedScript(scriptId: string): void;
selectOnly(scripts: ReadonlyArray<IScript>): void;
isSelected(script: IScript): boolean;
isSelected(scriptId: string): boolean;
selectAll(): void;
deselectAll(): void;
}
34 changes: 31 additions & 3 deletions src/application/State/Selection/UserSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { IRepository } from '@/infrastructure/Repository/IRepository';

export class UserSelection implements IUserSelection {
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript> = new InMemoryRepository<string, SelectedScript>();
private readonly scripts: IRepository<string, SelectedScript>;

constructor(
private readonly app: IApplication,
/** Initially selected scripts */
selectedScripts: ReadonlyArray<IScript>) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
if (selectedScripts && selectedScripts.length > 0) {
for (const script of selectedScripts) {
const selected = new SelectedScript(script, false);
Expand All @@ -22,6 +23,33 @@ export class UserSelection implements IUserSelection {
}
}

public removeAllInCategory(categoryId: number): void {
const category = this.app.findCategory(categoryId);
const scriptsToRemove = category.getAllScriptsRecursively()
.filter((script) => this.scripts.exists(script.id));
if (!scriptsToRemove.length) {
return;
}
for (const script of scriptsToRemove) {
this.scripts.removeItem(script.id);
}
this.changed.notify(this.scripts.getItems());
}

public addAllInCategory(categoryId: number): void {
const category = this.app.findCategory(categoryId);
const scriptsToAdd = category.getAllScriptsRecursively()
.filter((script) => !this.scripts.exists(script.id));
if (!scriptsToAdd.length) {
return;
}
for (const script of scriptsToAdd) {
const selectedScript = new SelectedScript(script, false);
this.scripts.addItem(selectedScript);
}
this.changed.notify(this.scripts.getItems());
}

public addSelectedScript(scriptId: string, revert: boolean): void {
const script = this.app.findScript(scriptId);
if (!script) {
Expand All @@ -44,8 +72,8 @@ export class UserSelection implements IUserSelection {
this.changed.notify(this.scripts.getItems());
}

public isSelected(script: IScript): boolean {
return this.scripts.exists(script.id);
public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptId);
}

/** Get users scripts based on his/her selections */
Expand Down
Loading

0 comments on commit b789250

Please sign in to comment.