Skip to content
Open
Show file tree
Hide file tree
Changes from 19 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
3 changes: 2 additions & 1 deletion src/assets/ts/components/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,12 @@ class RowComponent extends React.Component<RowProps, {}> {
linksStyle: getStyles(session.clientStore, ['theme-link']),
accentStyle: getStyles(session.clientStore, ['theme-text-accent']),
cursorBetween: this.props.cursorBetween,
session,
};

const hooksInfo = {
path, pluginData: this.props.cached.pluginData,
has_cursor, has_highlight
has_cursor, has_highlight, lineData
};

lineoptions.lineHook = PartialUnfolder.trivial<Token, React.ReactNode>();
Expand Down
7 changes: 6 additions & 1 deletion src/assets/ts/components/line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
EmitFn, Token, Tokenizer, PartialTokenizer,
RegexTokenizerSplitter, CharInfo, Unfolder, PartialUnfolder
} from '../utils/token_unfolder';
import Session from '../session';

export type LineProps = {
lineData: Line;
Expand All @@ -21,6 +22,7 @@ export type LineProps = {
wordHook?: PartialUnfolder<Token, React.ReactNode>;
onCharClick?: ((col: Col, e: Event) => void) | undefined;
cursorBetween?: boolean;
session?: Session;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be better to not have the Session abstraction here and just pass in something like

renderLineAnnotationHook?: (info: { lineData: .., column: ..., cursors: ...}) => Array<React.ReactNode>,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got it working that way, but I had to bind the session to the function. Does that defeat the purpose? Genuinely curious exactly why this way is cleaner.

Copy link
Owner

@WuTheFWasThat WuTheFWasThat Apr 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, fine to bind the session to the function from the caller. i don't know if it really matters, was just going broadly by the principle of requiring less abstractions in interfaces :)

for example if you wrote a unit test for line.tsx it would be maybe annoying if you had to construct a session object to test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, thanks for explaining!

};

// NOTE: hacky! we don't include .:/?= since urls contain it
Expand All @@ -40,6 +42,7 @@ export default class LineComponent extends React.Component<LineProps, {}> {
const cursors = this.props.cursors || {};
const highlights = this.props.highlights || {};
const accents = this.props.accents || {};
const session = this.props.session;

// ideally this takes up space but is unselectable (uncopyable)
const cursorChar = ' ';
Expand Down Expand Up @@ -106,6 +109,7 @@ export default class LineComponent extends React.Component<LineProps, {}> {
onClick = this.props.onCharClick.bind(this, column);
}
}
const children = session?.applyHook('renderCharChildren', [], { lineData, column, cursors });
const divType = char_info.renderOptions.divType || 'span';
emit(
React.createElement(
Expand All @@ -118,7 +122,8 @@ export default class LineComponent extends React.Component<LineProps, {}> {
href: href,
target: target
} as React.DOMAttributes<any>,
token.text[i] as React.ReactNode
token.text[i] as React.ReactNode,
children
)
);
}
Expand Down
9 changes: 9 additions & 0 deletions src/assets/ts/definitions/basics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isLink } from '../utils/text';
import keyDefinitions, { Action, ActionContext, SequenceAction } from '../keyDefinitions';
import _, { stubObject } from 'lodash';

keyDefinitions.registerAction(new Action(
'move-cursor-normal',
Expand Down Expand Up @@ -28,6 +29,11 @@ keyDefinitions.registerAction(new Action(
if (motion == null) {
throw new Error('Motion command was not passed a motion');
}
context.keyStream.save();
const action = _.last(context.keyStream.lastSequence);
const struct = { preventDefault: false };
await session.applyHookAsync('move-cursor-insert', struct, { action });
if (struct.preventDefault) { return; };
await motion(session.cursor, {pastEnd: true});
},
{ acceptsMotion: true },
Expand Down Expand Up @@ -513,6 +519,9 @@ keyDefinitions.registerAction(new Action(
'split-line',
'Split line at cursor',
async function({ session }) {
const struct = {preventDefault: false};
await session.applyHookAsync('split-line', struct, {});
if (struct.preventDefault) { return; }
await session.newLineAtCursor();
},
));
Expand Down
139 changes: 131 additions & 8 deletions src/plugins/marks/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getStyles } from '../../assets/ts/themes';
import { SINGLE_LINE_MOTIONS } from '../../assets/ts/definitions/motions';
import { INSERT_MOTION_MAPPINGS } from '../../assets/ts/configurations/vim';
import { motionKey } from '../../assets/ts/keyDefinitions';
import { ChangeChars } from '../../assets/ts/mutations';

// TODO: do this elsewhere
declare const process: any;
Expand Down Expand Up @@ -54,6 +55,8 @@ export class MarksPlugin {
public SetMark!: new(row: Row, mark: Mark) => Mutation;
public UnsetMark!: new(row: Row) => Mutation;
private marks_to_paths: {[mark: string]: Path};
private autocomplete_idx: number;
private autocomplete_matches: string[];

constructor(api: PluginApi) {
this.api = api;
Expand All @@ -63,6 +66,8 @@ export class MarksPlugin {
// NOTE: this may not be initialized correctly at first
// this only affects rendering @marklinks for now
this.marks_to_paths = {};
this.autocomplete_idx = 0;
this.autocomplete_matches = [];
}

public async enable() {
Expand Down Expand Up @@ -175,7 +180,7 @@ export class MarksPlugin {
},
key_transforms: [
async (key, context) => {
if (key === 'space') { key = ' '};
if (key === 'space') { key = ' '; };
if (key.length === 1) {
if (this.markstate === null) {
throw new Error('Mark state null during key transform');
Expand Down Expand Up @@ -219,13 +224,7 @@ export class MarksPlugin {
async function({ session }) {
return async cursor => {
const line = await session.document.getText(cursor.row);
const matches = that.getMarkMatches(line);
let mark = '';
matches.map((pos) => {
if (cursor.col >= pos[0] && cursor.col <= pos[1]) {
mark = that.parseMarkMatch(line.slice(pos[0], pos[1]));
}
});
const mark = that.getMarkUnderCursor(line, cursor.col);
if (!mark) {
session.showMessage(`Cursor should be over a mark link`);
return;
Expand Down Expand Up @@ -395,6 +394,7 @@ export class MarksPlugin {
return options;
});

// Renders mark to the left of line
this.api.registerHook('session', 'renderLineContents', (lineContents, info) => {
const { pluginData } = info;
if (pluginData.marks) {
Expand Down Expand Up @@ -438,7 +438,71 @@ export class MarksPlugin {
return lineContents;
});

// Renders autocomplete menu
this.api.registerHook('session', 'renderCharChildren', (children, info) => {
const { lineData, column, cursors } = info;
const line: string = lineData.join('');
const cursor = this.session.cursor;
if (this.session.mode === 'INSERT' && Object.keys(cursors).length > 0) {
const matches = this.getMarkMatches(line);
let inAutocomplete = false;
matches.map(pos => {
const start = pos[0], end = pos[1];
if (cursor.col >= start + 1 && cursor.col <= end) {
inAutocomplete = true;
if (start === column) {
const query = this.parseMarkMatch(line.slice(start, end));
this.autocomplete_matches = this.searchMark(query).slice(0, 10); // only show first 10 results
const n = matches.length;
if (n === 0) {
this.autocomplete_idx = 0;
return;
}
children.push(
<span key='autocompleteAnchor'
style={{
position: 'relative'
}}>
<span key='autocompleteContainer'
style={{
...getStyles(this.api.session.clientStore, ['theme-bg-tertiary']),
position: 'absolute',
zIndex: 1000,
width: '200px',
top: '1.2em'
}}
>
{this.autocomplete_matches.map((mark, idx) => {
const theme = (this.autocomplete_idx === idx) ? 'theme-bg-secondary' : 'theme-bg-tertiary';
return (
<div key={`autocomplete-row-${idx}`}
style={{
...getStyles(this.api.session.clientStore, [theme]),
}}>
{mark}
</div>
);
})}
</span>
</span>
);
}

}
});
if (!inAutocomplete) {
this.autocomplete_idx = 0;
this.autocomplete_matches = [];
}
}
if (this.session.mode !== 'INSERT') {
this.autocomplete_idx = 0;
this.autocomplete_matches = [];
}
return children;
});

// Renders mark links
this.api.registerHook('session', 'renderLineTokenHook', (tokenizer) => {
return tokenizer.then(new PartialUnfolder<Token, React.ReactNode>((
token: Token, emit: EmitFn<React.ReactNode>, wrapped: Tokenizer
Expand Down Expand Up @@ -466,6 +530,46 @@ export class MarksPlugin {
emit(...wrapped.unfold(token));
}));
});

// Handles up and down in autocomplete
this.api.registerHook('session', 'move-cursor-insert', async (struct, info) => {
const { action } = info;
const line = await that.document.getText(this.session.cursor.row);
const curMark = that.getMarkUnderCursor(line, this.session.cursor.col);
if (curMark === null) { return; };
const n = this.autocomplete_matches.length;
if (action === 'down') {
struct.preventDefault = true;
this.autocomplete_idx = ((this.autocomplete_idx % n) + n + 1) % n;
}
if (action === 'up') {
struct.preventDefault = true;
this.autocomplete_idx = ((this.autocomplete_idx % n) + n + n - 1) % n;
}
});

// Handles enter in autocomplete
this.api.registerHook('session', 'split-line', async (struct) => {
const line = await that.document.getText(this.session.cursor.row);
const curMark = that.getMarkUnderCursor(line, this.session.cursor.col);
if (curMark === null) { return; };
if (this.autocomplete_matches) {
struct.preventDefault = true;
// Set mark text
const matches = this.getMarkMatches(line);
const cursor = this.session.cursor;
const match = this.autocomplete_matches[this.autocomplete_idx];
await Promise.all(matches.map(async pos => {
if (cursor.col >= pos[0] && cursor.col <= pos[1]) {
const start = line[pos[0]] === '@' ? pos[0] + 1 : pos[0] + 2;
const end = line[pos[0]] === '@' ? pos[1] : pos[1] - 2;
const mutation = new ChangeChars(cursor.row, start, end - start, undefined, match.split(''));
await this.session.do(mutation);
cursor.col = start + match.length;
}
}));
}
});
this.api.registerListener('document', 'afterDetach', async () => {
this.computeMarksToPaths(); // FIRE AND FORGET
});
Expand Down Expand Up @@ -633,6 +737,25 @@ export class MarksPlugin {
const mark = match.slice(markStart, markEnd).replace(/(\.|!|\?)+$/g, '');
return mark;
}

public getMarkUnderCursor(line: string, col: Col): string | null {
const matches = this.getMarkMatches(line);
let mark = null;
matches.map((pos) => {
if (col >= pos[0] && col <= pos[1]) {
mark = this.parseMarkMatch(line.slice(pos[0], pos[1]));
}
});
return mark;
}

private searchMark(query: string) {
const marks = Object.keys(this.marks_to_paths);
const matches = marks.filter(mark => {
return mark.includes(query);
}).sort();
return matches;
}
}

// NOTE: because listing marks filters, disabling is okay
Expand Down