Skip to content

Make whitespace inside selections visible #397

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

Merged
merged 19 commits into from
Jun 19, 2025

Conversation

pheonick
Copy link
Contributor

@pheonick pheonick commented Jun 2, 2025

As a console editor user I often need to see spaces and tabs visually.

Added View->Toggle Render Whitespace checkbox menu option.
Added localizations, borrowed from https://github.com/microsoft/vscode-loc.
All new functionality is only executed if the toggle is enabled.

Any suggestions are welcome.

Closes #34

@pheonick
Copy link
Contributor Author

pheonick commented Jun 2, 2025

@microsoft-github-policy-service agree

@lhecker
Copy link
Member

lhecker commented Jun 3, 2025

Have you seen #188? I described there that we can't really use U+00B7 Middle Dot (·).
Also, I'd prefer if we only show these invisibles inside the selection for now, without a menu toggle.

@pheonick
Copy link
Contributor Author

pheonick commented Jun 3, 2025

No, sorry, the issue didn't mention any of the usual words I'd expect such as "render" or "whitespace", so a quick search didn't return anything and I just jumped into coding.

Example of current view:
image

Example of U+2219:
image

It's quite a lot thicker for the default Cascadia Mono font of the Windows Terminal, but then again also very visible if we'd only render them inside of the selection:
image

There's another option of U+2E31 that I've seen in https://github.com/microsoft/vscode/blob/c2c80e903fd500914164d1a45a57a52ccf99369f/src/vs/editor/browser/viewParts/whitespace/whitespace.ts#L158
But the dot is left aligned which is kind of ugly and much harder to see:
image
And the always on version, in which it looks better than U+2219 if it was always on:
image

Personally I always have them rendered with the "all" setting in VSCode and never even realized that the default option is "selection".

Screen.Recording.2025-06-03.135816.mp4

Implementing blending of whitespaces with the selection's bright blue color would require to check chars 1 by 1 inside of the selection logic.
Implementing blending of whitespaces into the focused line's background would also require to check chars 1 by 1.

For now I have 3 questions then, @lhecker:

  • Should we go with U+2219 or U+2E31?
  • Do we want them to be blended with the blue selection color? (In the mp4 above they aren't, a bit difficult to because we don't read chars 1 by 1 during selection)
  • Should we render them on the focused line? (As seen in the mp4 above, not rendering would also require to check the focused line's chars 1 by 1)

@pheonick pheonick force-pushed the feature/toggle-render-whitespace branch from 1123a3a to 9679e1e Compare June 3, 2025 20:12
@pheonick
Copy link
Contributor Author

pheonick commented Jun 3, 2025

I've kept playing around with the code and got to something I find usable.

image

In order to not have to keep track of coloring and hiding the text as I for some reason assumed I had to do when I asked my questions (didn't want to let go of my initial code lol), I create a second line with rendered whitespaces in parallel to the line that is rendered by default.
Then when the user does a selection, I replace the selected text from default to the whitespace rendered one.

This does sound a bit crazy space-wise when I think about it, but I think I ran out of any other ideas.
Selection of all text previously reported 6459B, now it's 10200B.
Task Manager reported 736KB, now it's 752KB.

If this seems like something usable, I'm ready for feedback.

Copy link
Member

@lhecker lhecker left a comment

Choose a reason for hiding this comment

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

While the overall approach is quite alright, I'm not yet happy with the performance and compactness aspects of this PR. For instance, the replace() call defeats the purpose of the arena allocator.

The char_indices code is incorrect, because logical coordinates are in grapheme clusters, and not in codepoints.

The way I would do it is by first building the line as it's currently done. Then, within the selection code, I would build up an entirely separate copy of line where spaces/tabs are replaced with visualizers. Once that's done, we can replace the original line with the copy.

Within this logic we'd be searching for spaces/tabs to replace, so we can also use this opportunity to change the foreground color right then and there before writing the text.

Moving the scratch_area call into the outer loop may help with all that.

I can help with these changes later, if I have time. 🙂

@pheonick
Copy link
Contributor Author

image

Instead of making a new line, I've decided to just replace chars and set color as iterate through them.

This works perfectly and I don't feel like I did any dirty workarounds this time.

There are issues with the Word Wrap in conjunction with tabs in the main branch that I've noticed as of 1.0 release, but never got to report though. Now raised in #479

This bug also kills any rendering around the broken lines:

2025-06-14.02-36-14.mp4

@pheonick pheonick requested a review from lhecker June 13, 2025 23:37
@lhecker lhecker changed the title Added "Toggle Render Whitespace" feature Make whitespace inside selections visible Jun 19, 2025
@lhecker lhecker requested a review from DHowett June 19, 2025 00:52
@lhecker
Copy link
Member

lhecker commented Jun 19, 2025

I've rewritten your PR quite significantly. Your previous approach wasn't necessarily worse than the new one, it's just that this approach is shorter. It also ties into another problem that I needed to solve: #34. I figured by rewriting it like this I can kill two birds with one stone.

@DHowett
Copy link
Member

DHowett commented Jun 19, 2025

Thanks @pheonick for the first pass at this!

@pheonick
Copy link
Contributor Author

Thank you guys, sorry I forgot to press the Submit Review button after taking a look and testing the changes a few hours ago.
I suggest to take a look before merging.

@lhecker lhecker closed this Jun 19, 2025
@lhecker lhecker reopened this Jun 19, 2025
@lhecker
Copy link
Member

lhecker commented Jun 19, 2025

I was trying to click into the comment field while I was pushing a commit in the background. This caused the page contents to shift, and I clicked the "Close PR" button... 😅

In any case, thank you for reviewing my changes! I reverted it back to a version you suggested. 🙂


// The start of the selection is within this line. We need to update selection_beg.
if selection_beg <= cursor_end.logical_pos
&& selection_beg >= cursor_beg.logical_pos
{
cursor = self.cursor_move_to_logical_internal(
cursor,
Point { x: selection_beg.x.max(origin.x), y: selection_beg.y },
Point { x: selection_beg.x, y: selection_beg.y },
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this just...

Suggested change
Point { x: selection_beg.x, y: selection_beg.y },
selection_beg,

@@ -1776,18 +1775,18 @@ impl TextBuffer {
{
cursor = self.cursor_move_to_logical_internal(
cursor,
Point { x: selection_end.x.min(origin.x + text_width), y: selection_end.y },
Point { x: selection_end.x, y: selection_end.y },
Copy link
Member

Choose a reason for hiding this comment

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

ut supra selection_end

@pheonick
Copy link
Contributor Author

Works perfectly both as is and with suggestions above applied.
I'm happy with this code to be merged.

@pheonick
Copy link
Contributor Author

This being my first open source PR I'm not fully equipped with the proper etiqutte in such small review update cases, so I took it upon myself to push the review fixes, sorry if I'm stepping on @lhecker's toes here.

@pheonick pheonick requested a review from DHowett June 19, 2025 22:10
@lhecker
Copy link
Member

lhecker commented Jun 19, 2025

I think it's a lot ruder for me to push into your PR. You're definitely not stepping on my toes. 😄

@lhecker lhecker enabled auto-merge (squash) June 19, 2025 22:17
@pheonick
Copy link
Contributor Author

One day I'll add autofmt on file save, embarrassing...

@lhecker lhecker disabled auto-merge June 19, 2025 22:18
@lhecker lhecker enabled auto-merge (squash) June 19, 2025 22:18
Copy link
Member

@DHowett DHowett left a comment

Choose a reason for hiding this comment

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

Yall are the best. Thank you :)

@lhecker lhecker merged commit 796209f into microsoft:main Jun 19, 2025
3 checks passed
@lhecker
Copy link
Member

lhecker commented Jun 19, 2025

FYI: Long-term I think that this code will probably move into framebuffer.rs. This is because labels and other UI elements should also be scanned for "dangerous" C0 and C1 control codes. I'm not sure how it would look like, but I imagine that we would add APIs like Framebuffer::draw_text_with_visual_whitespace or something of that sort. I think this would simplify the already fairly complex TextBuffer implementation and make it somewhat more manageable. It would then also make it a lot easier to add "never / always show whitespace" settings.

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.

Escape C1 control characters when printing text buffer contents
3 participants