Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for markdown cells in positron notebooks #2563

Merged
merged 58 commits into from
Apr 1, 2024

Conversation

nstrayer
Copy link
Contributor

Intent

Add the ability to create and edit markdown cells in notebooks.

image

Approach

Leveraged existing markdown parser from the markdown-language-features extension.

  • Added new component for displaying images (DeferredImage) from the local file-system in react-land.

    • Uses new extension positron-notebooks to do so with the 'positronNotebookHelpers.convertImageToBase64' command that converts image to data-url.
    • Updated the renderHtml() function to allow for the usage of custom components for a tag type. This is used to pass <DeferredImage/> component instead of plain <img/>.
  • Added <Markdown/> component that takes a string of markdown text and renders it into react land with images done using the <DeferredImage/> component

  • Markup cells default to closed but can be opened via double-clicking or a "show editor" button.

Addresses #2350

Copy link
Contributor

@seeM seeM left a comment

Choose a reason for hiding this comment

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

(Partial review since I had to head out, will complete tomorrow.)

Very nice! It mostly worked well in my testing. A few rough edges I encountered and some thoughts below, but happy to get :

  1. For some reason the "Hide Editor" button didn't initially work for me, but then started working and I'm not sure what changed.
  2. I also managed to make my notebook tab go blank although I'm not sure how. I think I tried to close the tab (but it didn't actually close) then created a markdown cell and added an image to it.
  3. Links are not yet clickable.
  4. We'll want to decide which approach we take in terms of showing a side-by-side preview versus previewing when the user tries to "run" the cell, but we can handle that in future.

We can address some of these in follow-ups of course.

@nstrayer nstrayer force-pushed the feature/notebooks/markdown-cells branch from b8b36df to f2fcdfb Compare March 29, 2024 17:26

React.useEffect(() => {
const timeoutMs = 3000;
const convertCommand = commandWithTimeout<unknown>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the onError part of this work i.e. can you catch errors in the extension host by try-catching executeCommand?

I'm still able to see errors in the console if they aren't caught in the command.

Copy link
Contributor

Choose a reason for hiding this comment

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

Something weird also happens if I use a URL e.g. https://posit.co/wp-content/uploads/2023/03/home-hero-connect-e1689269684616.jpg. It displays in the preview, but also shows an error in the console:

Error: ENOENT: no such file or directory, open '/Users/seem/posit/demos/https:/posit.co/wp-content/uploads/2023/03/home-hero-connect-e1689269684616.jpg' 

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Errors thrown in the extension context don't seem to trigger errors in the ui-thread. So in that case onError() really only catches errors where no data is returned. Probably worth renaming or consolidating that logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cleaned up this logic in b7402a8 to make it more obvious what was going on. Unhandled errors in the extension will now most likely trigger a timeout now, which seems reasonable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ff2afc6 Lets remote images pass through (Based on if it starts with http or https which I think is safe enough.

nstrayer and others added 23 commits April 1, 2024 10:49
…rvable renderedHtml field for the markup cells.
…ed markup view. Also fix distracting action button outlines.
@nstrayer nstrayer force-pushed the feature/notebooks/markdown-cells branch from 62800ce to 0d5a5e3 Compare April 1, 2024 14:50
Copy link
Contributor

@softwarenerd softwarenerd left a comment

Choose a reason for hiding this comment

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

I found just one small thing.

Comment on lines 13 to 32
export function promiseWithTimeout<T>(promise: Promise<T>, timeoutMs: number, cancelToken: CancellationToken): Promise<T> {

return new Promise((resolve, reject) => {
cancelToken.onCancellationRequested(() => {
reject(new Error('Promise cancelled'));
});

const timeout = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeoutMs}ms`));
}, timeoutMs);

promise.then((res) => {
clearTimeout(timeout);
resolve(res);
}).catch((err) => {
clearTimeout(timeout);
reject(err);
});
});
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jmcphers Is this reinventing a wheel from somewhere in the codebase already?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is. I think you want raceTimeout here.

Comment on lines 13 to 32
export function promiseWithTimeout<T>(promise: Promise<T>, timeoutMs: number, cancelToken: CancellationToken): Promise<T> {

return new Promise((resolve, reject) => {
cancelToken.onCancellationRequested(() => {
reject(new Error('Promise cancelled'));
});

const timeout = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeoutMs}ms`));
}, timeoutMs);

promise.then((res) => {
clearTimeout(timeout);
resolve(res);
}).catch((err) => {
clearTimeout(timeout);
reject(err);
});
});
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

It is. I think you want raceTimeout here.

@nstrayer nstrayer merged commit 0aa7255 into main Apr 1, 2024
1 check passed
@nstrayer nstrayer deleted the feature/notebooks/markdown-cells branch April 1, 2024 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants