Skip to content

Commit

Permalink
GH-1479: Initial cut of the tree search.
Browse files Browse the repository at this point in the history
Closes #1479.
Closes #1493.

Signed-off-by: Akos Kitta <[email protected]>
  • Loading branch information
kittaakos committed Mar 15, 2018
1 parent bb02ba4 commit cc62aeb
Show file tree
Hide file tree
Showing 22 changed files with 1,209 additions and 39 deletions.
50 changes: 37 additions & 13 deletions packages/core/src/browser/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import { isOSX } from '../common/os';
* Since `M2+M3+<Key>` (Alt+Shift+<Key>) is reserved on MacOS X for writing special characters, such bindings are commonly
* undefined for platform MacOS X and redefined as `M1+M3+<Key>`. The rule applies on the `M3+M2+<Key>` sequence.
*/
export declare type Keystroke = { first?: Key, modifiers?: KeyModifier[] };
export interface Keystroke {
readonly first?: Key;
readonly modifiers?: KeyModifier[];
}

export type KeySequence = KeyCode[];
export namespace KeySequence {
Expand Down Expand Up @@ -82,9 +85,9 @@ export namespace KeySequence {
return keyCodes;
}

export function acceleratorFor(keysequence: KeySequence, separator: string = " ") {
export function acceleratorFor(keySequence: KeySequence, separator: string = " ") {
const result: string[] = [];
for (const keyCode of keysequence) {
for (const keyCode of keySequence) {
let keyCodeResult = "";
let previous = false;

Expand Down Expand Up @@ -310,8 +313,8 @@ export class KeyCode {
throw new Error(`Cannot get key code from the keyboard event: ${event}.`);
}

public static equals(keycode1: KeyCode, keycode2: KeyCode): boolean {
return JSON.stringify(keycode1) === JSON.stringify(keycode2);
public static equals(keyCode1: KeyCode, keyCode2: KeyCode): boolean {
return JSON.stringify(keyCode1) === JSON.stringify(keyCode2);
}

equals(event: KeyboardEvent | KeyCode): boolean {
Expand Down Expand Up @@ -365,6 +368,15 @@ export class KeyCode {
}
}

export namespace KeyCode {

/**
* Determines a `true` of `false` value for the key code argument.
*/
export type Predicate = (keyCode: KeyCode) => boolean;

}

export enum KeyModifier {
/**
* M1 is the COMMAND key on MacOS X, and the CTRL key on most other platforms.
Expand Down Expand Up @@ -412,8 +424,15 @@ export namespace KeyModifier {
}
}

export declare type Key = { readonly code: string, readonly keyCode: number };
export declare type EasyKey = { readonly keyCode: number, readonly easyString: string };
export interface Key {
readonly code: string;
readonly keyCode: number;
}

export interface EasyKey {
readonly keyCode: number;
readonly easyString: string;
}

const CODE_TO_KEY: { [code: string]: Key } = {};
const KEY_CODE_TO_KEY: { [keyCode: number]: Key } = {};
Expand Down Expand Up @@ -541,11 +560,12 @@ export namespace EasyKey {

export namespace Key {

// tslint:disable-next-line:no-any
export function isKey(arg: any): arg is Key {
return !!arg && ('code' in arg) && ('keyCode' in arg);
}

export function getKey(arg: string | number) {
export function getKey(arg: string | number): Key {
if (typeof arg === "number") {
return KEY_CODE_TO_KEY[arg] || {
code: 'unknown',
Expand All @@ -556,17 +576,21 @@ export namespace Key {
}
}

export function getEasyKey(key: Key) {
export function getEasyKey(key: Key): EasyKey {
return KEY_CODE_TO_EASY[key.keyCode];
}

export function isModifier(arg: string | number) {
export function isModifier(arg: string | number): boolean {
if (typeof arg === "number") {
return MODIFIERS.map(key => key.keyCode).indexOf(arg) > 0;
}
return MODIFIERS.map(key => key.code).indexOf(arg) > 0;
}

export function equals(key: Key, keyCode: KeyCode): boolean {
return !!keyCode.key && key.keyCode === keyCode.key.keyCode;
}

export const ENTER: Key = { code: "Enter", keyCode: 13 };
export const SPACE: Key = { code: "Space", keyCode: 32 };
export const TAB: Key = { code: "Tab", keyCode: 9 };
Expand Down Expand Up @@ -681,9 +705,9 @@ export namespace Key {
});
MODIFIERS.push(...[Key.ALT_LEFT, Key.ALT_RIGHT, Key.CONTROL_LEFT, Key.CONTROL_RIGHT, Key.O_S_LEFT, Key.O_S_RIGHT, Key.SHIFT_LEFT, Key.SHIFT_RIGHT]);

Object.keys(EasyKey).map(prop => Reflect.get(EasyKey, prop)).forEach(easykey => {
EASY_TO_KEY[easykey.easyString] = KEY_CODE_TO_KEY[easykey.keyCode];
KEY_CODE_TO_EASY[easykey.keyCode] = easykey;
Object.keys(EasyKey).map(prop => Reflect.get(EasyKey, prop)).forEach(easyKey => {
EASY_TO_KEY[easyKey.easyString] = KEY_CODE_TO_KEY[easyKey.keyCode];
KEY_CODE_TO_EASY[easyKey.keyCode] = easyKey;
});
})();

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/browser/style/tree-decorators.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

.theia-TreeNodeSegment mark {
background: var(--theia-highlight-background);
color: var(--theia-highlight-color);
}

.theia-caption-prefix {
white-space: nowrap;
padding-right: 2px;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/browser/style/tree.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
font-size: var(--theia-ui-font-size1);
transform: translateY(var(--theia-border-width));
max-height: calc(100% - var(--theia-border-width));
position: relative;
}

.theia-Tree:focus {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/browser/style/variables-bright.useable.css
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ all of MD as it is not optimized for dense, information rich UIs.
--theia-removed-color0: rgba(230, 0, 0, 0.8);
--theia-modified-color0: rgba(0, 100, 150, 0.8);

--theia-highlight-background: rgba(234, 92, 0, 0.33);
--theia-highlight-color: var(--theia-content-font-color0);

/* icons */
--theia-icon-close: url(../icons/close-bright.svg);
--theia-sprite-y-offset: 0px;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/browser/style/variables-dark.useable.css
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ all of MD as it is not optimized for dense, information rich UIs.
--theia-removed-color0: rgba(230, 0, 0, 0.8);
--theia-modified-color0: rgba(0, 100, 150, 0.8);

--theia-highlight-background: rgba(234, 92, 0, 0.33);
--theia-highlight-color: var(--theia-content-font-color0);

/* icons */
--theia-icon-close: url(../icons/close-dark.svg);
--theia-sprite-y-offset: -20px;
Expand Down
32 changes: 32 additions & 0 deletions packages/core/src/browser/tree/tree-decorator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,36 @@ describe('tree-decorator', () => {

});

describe('caption-highligh', () => {

describe('range-contains', () => {
([
[1, 2, 3, false],
[0, 0, 1, true],
[1, 0, 1, true],
[1, 1, 1, true],
[2, 1, 1, true],
[3, 1, 1, false],
[1, 1, -100, false],
] as [number, number, number, boolean][]).forEach(test => {
const [input, offset, length, expected] = test;
it(`${input} should ${expected ? '' : 'not '}be contained in the [${offset}:${length}] range`, () => {
expect(TreeDecoration.CaptionHighlight.Range.contains(input, { offset, length })).to.be.equal(expected);
});
});
});

it('split', () => {
const actual = TreeDecoration.CaptionHighlight.split('alma', {
ranges: [{ offset: 0, length: 1 }]
});
expect(actual).has.lengthOf(2);
expect(actual[0].highligh).to.be.true;
expect(actual[0].data).to.be.equal('a');
expect(actual[1].highligh).to.be.undefined;
expect(actual[1].data).to.be.equal('lma');
});

});

});
107 changes: 107 additions & 0 deletions packages/core/src/browser/tree/tree-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export namespace TreeDecoration {
* CSS styles for the tree decorators.
*/
export namespace Styles {
export const CAPTION_HIGHLIGHT_CLASS = 'theia-caption-highlight';
export const CAPTION_PREFIX_CLASS = 'theia-caption-prefix';
export const CAPTION_SUFFIX_CLASS = 'theia-caption-suffix';
export const ICON_WRAPPER_CLASS = 'theia-icon-wrapper';
Expand Down Expand Up @@ -313,6 +314,107 @@ export namespace TreeDecoration {

}

/**
* The caption highlighting with the highlighted ranges and an optional background color.
*/
export interface CaptionHighlight {

/**
* The ranges to highlight in the caption.
*/
readonly ranges: CaptionHighlight.Range[]

/**
* The optional color of the text data that is being highlighted. Falls back to the default `mark` color values defined under a tree node segment class.
*/
readonly color?: Color;

/**
* The optional background color of the text data that is being highlighted.
*/
readonly backgroundColor?: Color;
}

export namespace CaptionHighlight {

/**
* A pair of offset and length that has to be highlighted as a range.
*/
export interface Range {

/**
* Zero based offset of the highlighted region.
*/
readonly offset: number;

/**
* The length of the highlighted region.
*/
readonly length: number;

}

export namespace Range {

/**
* `true` if the `arg` is contained in the range. The ranges are closed ranges, hence the check is inclusive.
*/
export function contains(arg: number, range: Range): boolean {
return arg >= range.offset && arg <= (range.offset + range.length);
}

}

/**
* The result of a caption splitting based on the highlighting information.
*/
export interface Fragment {

/**
* The text data of the fragment.
*/
readonly data: string;

/**
* Has to be highlighted if defined.
*/
readonly highligh?: true

}

/**
* Splits the `caption` argument based on the ranges from the `highlight` argument.
*/
export function split(caption: string, highlight: CaptionHighlight): Fragment[] {
const result: Fragment[] = [];
const ranges = highlight.ranges.slice();
const containerOf = (index: number) => ranges.findIndex(range => Range.contains(index, range));
let data = '';
for (let i = 0; i < caption.length; i++) {
const containerIndex = containerOf(i);
if (containerIndex === -1) {
data += caption[i];
} else {
if (data.length > 0) {
result.push({ data });
}
const { length } = ranges.splice(containerIndex, 1).shift()!;
result.push({ data: caption.substr(i, length), highligh: true });
data = '';
i = i + length - 1;
}
}
if (data.length > 0) {
result.push({ data });
}
if (ranges.length !== 0) {
throw new Error(`Error occurred when splitting the caption. There was a mismatch between the caption and the corresponding highlighting ranges.`);
}
return result;
}

}

/**
* Encapsulates styling information that has to be applied on the tree node which we decorate.
*/
Expand Down Expand Up @@ -365,6 +467,11 @@ export namespace TreeDecoration {
*/
readonly iconOverlay?: IconOverlay;

/**
* An array of ranges to highlight the caption.
*/
readonly highlight?: CaptionHighlight;

}

export namespace Data {
Expand Down
58 changes: 57 additions & 1 deletion packages/core/src/browser/tree/tree-iterator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { TreeNavigationService } from './tree-navigation';
import { TreeModel, TreeModelImpl } from './tree-model';
import { TreeExpansionService, TreeExpansionServiceImpl, ExpandableTreeNode } from './tree-expansion';
import { TreeSelectionService, TreeSelectionServiceImpl } from './tree-selection';
import { DepthFirstTreeIterator, BreadthFirstTreeIterator, BottomUpTreeIterator, TopDownTreeIterator } from './tree-iterator';
import { DepthFirstTreeIterator, BreadthFirstTreeIterator, BottomUpTreeIterator, TopDownTreeIterator, Iterators } from './tree-iterator';

// tslint:disable:no-unused-expression
// tslint:disable:max-line-length
Expand Down Expand Up @@ -119,3 +119,59 @@ describe('tree-iterator', () => {
}

});

describe('iterators', () => {

it('as-iterator', () => {
const array = [1, 2, 3, 4];
const itr = Iterators.asIterator(array);
let next = itr.next();
while (!next.done) {
const { value } = next;
expect(value).to.be.not.undefined;
const index = array.indexOf(value);
expect(index).to.be.not.equal(-1);
array.splice(index, 1);
next = itr.next();
}
expect(array).to.be.empty;
});

it('cycle - without start', function () {
this.timeout(1000);
const array = [1, 2, 3, 4];
const itr = Iterators.cycle(array);
const visitedItems = new Set();
let next = itr.next();
while (!next.done) {
const { value } = next;
expect(value).to.be.not.undefined;
if (visitedItems.has(value)) {
expect(Array.from(visitedItems).sort()).to.be.deep.equal(array.sort());
break;
}
visitedItems.add(value);
next = itr.next();
}
});

it('cycle - with start', function () {
this.timeout(1000);
const array = [1, 2, 3, 4];
const itr = Iterators.cycle(array, 2);
const visitedItems = new Set();
let next = itr.next();
expect(next.value).to.be.equal(2);
while (!next.done) {
const { value } = next;
expect(value).to.be.not.undefined;
if (visitedItems.has(value)) {
expect(Array.from(visitedItems).sort()).to.be.deep.equal(array.sort());
break;
}
visitedItems.add(value);
next = itr.next();
}
});

});
Loading

0 comments on commit cc62aeb

Please sign in to comment.