From e805fd665efbcb34576e8a66d78cc80d4e7e9730 Mon Sep 17 00:00:00 2001 From: Kat Hagan Date: Thu, 1 Oct 2020 17:58:58 -0700 Subject: [PATCH] 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 --- lib/components/note-preview/index.tsx | 10 ++++++- lib/note-content-editor.tsx | 39 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/lib/components/note-preview/index.tsx b/lib/components/note-preview/index.tsx index 706043150..449395828 100644 --- a/lib/components/note-preview/index.tsx +++ b/lib/components/note-preview/index.tsx @@ -20,6 +20,7 @@ type StateProps = { isFocused: boolean; note: T.Note | null; noteId: T.EntityId | null; + notes: Map; searchQuery: string; showRenderedView: boolean; }; @@ -37,6 +38,7 @@ export const NotePreview: FunctionComponent = ({ isFocused, note, noteId, + notes, openNote, searchQuery, showRenderedView, @@ -89,7 +91,12 @@ export const NotePreview: FunctionComponent = ({ } const [fullMatch, linkedNoteId] = match; - openNote(linkedNoteId as T.EntityId); + // if we try to open a note that doesn't exist in local state, + // then we annoyingly close the open note without opening anything else + // implicit else: links that aren't openable will just do nothing + if (notes.has(linkedNoteId as T.EntityId)) { + openNote(linkedNoteId as T.EntityId); + } return; } @@ -170,6 +177,7 @@ const mapStateToProps: S.MapState = (state, props) => { isFocused: state.ui.dialogs.length === 0 && !state.ui.showNoteInfo, note, noteId, + notes: state.data.notes, searchQuery: state.ui.searchQuery, showRenderedView: !!note?.systemTags.includes('markdown') && !state.ui.editMode, diff --git a/lib/note-content-editor.tsx b/lib/note-content-editor.tsx index 75178d53f..c8b42693f 100644 --- a/lib/note-content-editor.tsx +++ b/lib/note-content-editor.tsx @@ -63,6 +63,7 @@ type StateProps = { lineLength: T.LineLength; noteId: T.EntityId; note: T.Note; + notes: Map; searchQuery: string; spellCheckEnabled: boolean; theme: T.Theme; @@ -72,6 +73,7 @@ type DispatchProps = { clearSearch: () => any; editNote: (noteId: T.EntityId, changes: Partial) => any; insertTask: () => any; + openNote: (noteId: T.EntityId) => any; storeEditorSelection: ( noteId: T.EntityId, start: number, @@ -430,6 +432,41 @@ class NoteContentEditor extends Component { this.editor = editor; this.monaco = monaco; + monaco.languages.registerLinkProvider('plaintext', { + provideLinks: (model) => { + const matches = model.findMatches( + 'simplenote://note/[a-zA-Z0-9-]+', + true, // searchOnlyEditableRange + true, // isRegex + false, // matchCase + null, // wordSeparators + false // captureMatches + ); + return { + // don't set a URL on these links, because then Monaco skips resolveLink + // @cite: https://github.com/Microsoft/vscode/blob/8f89095aa6097f6e0014f2d459ef37820983ae55/src/vs/editor/contrib/links/getLinks.ts#L43:L65 + links: matches.map(({ range }) => ({ range })), + }; + }, + resolveLink: (link) => { + const href = editor.getModel()?.getValueInRange(link.range) ?? ''; + const match = /^simplenote:\/\/note\/(.+)$/.exec(href); + if (!match) { + return; + } + + const [fullMatch, linkedNoteId] = match as [string, T.EntityId]; + + // if we try to open a note that doesn't exist in local state, + // then we annoyingly close the open note without opening anything else + // implicit else: links that aren't openable will just do nothing + if (this.props.notes.has(linkedNoteId)) { + this.props.openNote(linkedNoteId); + } + return { ...link, url: '#' }; // tell Monaco to do nothing and not complain about it + }, + }); + // remove keybindings; see https://github.com/microsoft/monaco-editor/issues/287 const shortcutsToDisable = [ 'cancelSelection', // escape; we need to allow this to bubble up to clear search @@ -975,6 +1012,7 @@ const mapStateToProps: S.MapState = (state) => ({ lineLength: state.settings.lineLength, noteId: state.ui.openedNote, note: state.data.notes.get(state.ui.openedNote), + notes: state.data.notes, searchQuery: state.ui.searchQuery, spellCheckEnabled: state.settings.spellCheckEnabled, theme: selectors.getTheme(state), @@ -984,6 +1022,7 @@ const mapDispatchToProps: S.MapDispatch = { clearSearch: () => dispatch(search('')), editNote: actions.data.editNote, insertTask: () => ({ type: 'INSERT_TASK' }), + openNote: actions.ui.selectNote, storeEditorSelection: (noteId, start, end, direction) => ({ type: 'STORE_EDITOR_SELECTION', noteId,