Skip to content

Commit e805fd6

Browse files
authored
Internal links: use a Monaco link provider (#2376)
* use a link provider to linkify internal links * document args and limit regex to valid noteID characters * figured out how to skip the warning * remove types, stop overloading iLink with an additional attribute * more than zero * Dennis is awesome * handle bad note ID * Update note-content-editor.tsx clarify a comment * comment workshopping * preview component should also check for unopenable note * clean up conditional * types
1 parent aebfc22 commit e805fd6

File tree

2 files changed

+48
-1
lines changed

2 files changed

+48
-1
lines changed

lib/components/note-preview/index.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type StateProps = {
2020
isFocused: boolean;
2121
note: T.Note | null;
2222
noteId: T.EntityId | null;
23+
notes: Map<T.EntityId, T.Note>;
2324
searchQuery: string;
2425
showRenderedView: boolean;
2526
};
@@ -37,6 +38,7 @@ export const NotePreview: FunctionComponent<Props> = ({
3738
isFocused,
3839
note,
3940
noteId,
41+
notes,
4042
openNote,
4143
searchQuery,
4244
showRenderedView,
@@ -89,7 +91,12 @@ export const NotePreview: FunctionComponent<Props> = ({
8991
}
9092

9193
const [fullMatch, linkedNoteId] = match;
92-
openNote(linkedNoteId as T.EntityId);
94+
// if we try to open a note that doesn't exist in local state,
95+
// then we annoyingly close the open note without opening anything else
96+
// implicit else: links that aren't openable will just do nothing
97+
if (notes.has(linkedNoteId as T.EntityId)) {
98+
openNote(linkedNoteId as T.EntityId);
99+
}
93100
return;
94101
}
95102

@@ -170,6 +177,7 @@ const mapStateToProps: S.MapState<StateProps, OwnProps> = (state, props) => {
170177
isFocused: state.ui.dialogs.length === 0 && !state.ui.showNoteInfo,
171178
note,
172179
noteId,
180+
notes: state.data.notes,
173181
searchQuery: state.ui.searchQuery,
174182
showRenderedView:
175183
!!note?.systemTags.includes('markdown') && !state.ui.editMode,

lib/note-content-editor.tsx

+39
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type StateProps = {
6363
lineLength: T.LineLength;
6464
noteId: T.EntityId;
6565
note: T.Note;
66+
notes: Map<T.EntityId, T.Note>;
6667
searchQuery: string;
6768
spellCheckEnabled: boolean;
6869
theme: T.Theme;
@@ -72,6 +73,7 @@ type DispatchProps = {
7273
clearSearch: () => any;
7374
editNote: (noteId: T.EntityId, changes: Partial<T.Note>) => any;
7475
insertTask: () => any;
76+
openNote: (noteId: T.EntityId) => any;
7577
storeEditorSelection: (
7678
noteId: T.EntityId,
7779
start: number,
@@ -430,6 +432,41 @@ class NoteContentEditor extends Component<Props> {
430432
this.editor = editor;
431433
this.monaco = monaco;
432434

435+
monaco.languages.registerLinkProvider('plaintext', {
436+
provideLinks: (model) => {
437+
const matches = model.findMatches(
438+
'simplenote://note/[a-zA-Z0-9-]+',
439+
true, // searchOnlyEditableRange
440+
true, // isRegex
441+
false, // matchCase
442+
null, // wordSeparators
443+
false // captureMatches
444+
);
445+
return {
446+
// don't set a URL on these links, because then Monaco skips resolveLink
447+
// @cite: https://github.com/Microsoft/vscode/blob/8f89095aa6097f6e0014f2d459ef37820983ae55/src/vs/editor/contrib/links/getLinks.ts#L43:L65
448+
links: matches.map(({ range }) => ({ range })),
449+
};
450+
},
451+
resolveLink: (link) => {
452+
const href = editor.getModel()?.getValueInRange(link.range) ?? '';
453+
const match = /^simplenote:\/\/note\/(.+)$/.exec(href);
454+
if (!match) {
455+
return;
456+
}
457+
458+
const [fullMatch, linkedNoteId] = match as [string, T.EntityId];
459+
460+
// if we try to open a note that doesn't exist in local state,
461+
// then we annoyingly close the open note without opening anything else
462+
// implicit else: links that aren't openable will just do nothing
463+
if (this.props.notes.has(linkedNoteId)) {
464+
this.props.openNote(linkedNoteId);
465+
}
466+
return { ...link, url: '#' }; // tell Monaco to do nothing and not complain about it
467+
},
468+
});
469+
433470
// remove keybindings; see https://github.com/microsoft/monaco-editor/issues/287
434471
const shortcutsToDisable = [
435472
'cancelSelection', // escape; we need to allow this to bubble up to clear search
@@ -975,6 +1012,7 @@ const mapStateToProps: S.MapState<StateProps> = (state) => ({
9751012
lineLength: state.settings.lineLength,
9761013
noteId: state.ui.openedNote,
9771014
note: state.data.notes.get(state.ui.openedNote),
1015+
notes: state.data.notes,
9781016
searchQuery: state.ui.searchQuery,
9791017
spellCheckEnabled: state.settings.spellCheckEnabled,
9801018
theme: selectors.getTheme(state),
@@ -984,6 +1022,7 @@ const mapDispatchToProps: S.MapDispatch<DispatchProps> = {
9841022
clearSearch: () => dispatch(search('')),
9851023
editNote: actions.data.editNote,
9861024
insertTask: () => ({ type: 'INSERT_TASK' }),
1025+
openNote: actions.ui.selectNote,
9871026
storeEditorSelection: (noteId, start, end, direction) => ({
9881027
type: 'STORE_EDITOR_SELECTION',
9891028
noteId,

0 commit comments

Comments
 (0)