Q&A: Read-only regions

This question was asked recently on the (internal) editor discussion alias:

Is it at all possible to make parts of the text buffer read-only? Could I, for example, mark certain spans as not modifiable, so that the user wouldn't be able to change their contents?

The short answer is yes; in fact, marking regions of the buffer as read-only is the primary model of controlling the read-only-ness of the buffer, and doing something like setting the entire buffer read-only is a special case of that (marking the region that covers the entire buffer as read-only).

The general model

The API for creating read-only regions looks very much like creating regular edits. The entry point is to create a read-only region edit (ITextBuffer.CreateReadOnlyRegionEdit), which is what you use to make the actual edits of adding or removing read-only regions. To answer the original question, here's an example of how to make an entire buffer read-only:

 ITextBuffer textBuffer;
IReadOnlyRegion region;

using (IReadOnlyRegionEdit edit = textBuffer.CreateReadOnlyRegionEdit())
{
    region = edit.CreateReadOnlyRegion(new Span(0, edit.Snapshot.Length));
    edit.Apply();
}

When you create (add) a region, you get back a handle to the region, which you'll need to hold on to if you ever want to remove it again, like this:

 using (IReadOnlyRegionEdit edit = textBuffer.CreateReadOnlyRegionEdit())
{
    edit.RemoveReadOnlyRegion(region);
    edit.Apply();
}

In case you missed it, the (intentional) side-effect of this design is that only the "owner" who creates a read-only region can remove that region. This means that one person can mark parts of or the entire buffer read-only, thus preventing anyone else from editing it (as that person expects), without any way for other people to pull that protection out from under the original owner's proverbial feet.

To snapshot or not to snapshot

Sometime in the last year, we got reports that read-only regions were causing performance issues in general C# debugger stepping scenarios.

I don't remember the exact details, but the gist is that the C# language service was setting and unsetting read-only-ness on every step. There wasn't a simple way to determine that it was a "step" and not just a blanket "start running the debugger again", so it was a rather costly expense for what was essentially too short-lived for the user to even notice (or at least shouldn't be noticeable, assuming things are nice and fast).

At the time, generating new read-only regions was slow because each read-only region edit create a new ITextSnapshot for that buffer, with similar costs to making an edit. In this case, it was setting read-only regions on more than a single file, so I think it was more like making an edit on lots of files simultaneously.

The question, at the time, was, "why does the editor need to generate a new snapshot when read-only regions change?" It's an interesting design choice, to say the least. As I understand it (I wasn't on the team at the time, so this is hearsay), people thought there would be utility in being able to ask, "On snapshot foo, was region bar read-only?", to see what the read-only state of the buffer was historically, much like how snapshots let you look at the buffer contents historically.

As it turns out, though, that's not especially useful information to have. There is some symmetry there with asking what edits happened on old snapshots (technically versions), but there's isn't anything you can really do with that information. You can only edit the current version of the buffer, so the only real question people need to ask is, "On buffer foo, was region bar read-only?".

So, Jack changed read-only regions so that they are conceptually and literally buffer-specific instead of snapshot-specific, and most of that performance issue wasn't anymore. There were actually other changes made, in the language service, that made this change mostly unnecessary for that specific scenario, but it was still the change that we wanted to make.

Dynamic read-only regions

The other late entry to the game was the concept of dynamic read-only regions. This one was written by Sergei, at least in part to support (VS2008 and previous) markers, which can prevent edits over the span of text they cover.

These regions are inserted and removed just like regular ol' read-only regions, with one extra argument in CreateDynamicReadOnlyRegion: a DynamicReadOnlyRegionQuery. The contract for that callback is fairly simple; it's told whether or not the query ("is this read-only?") is happening as part of an edit or just as a simple request, and it returns true or false.

One reason for including information about whether the request is an edit is to use this feature for something like source code integration's check out on edit feature. You could imagine, for files that aren't yet checked-out, putting a dynamic read-only region over the file, returning true when asked about read-only-ness for non-edits, and then attempt to perform the checkout on edit.

This also leads to another guiding principle of read-only regions: if you need to make an edit, never ask about read-only regions before making the edit. Instead, try to make the edit and handle failure gracefully (the various methods on ITextEdit return whether or not that part of the edit succeeded). There are methods for asking about which spans in a buffer/snapshot are read-only, but you shouldn't use these if you intend to make an edit with the knowledge. As the saying goes, never ask permission, as for forgiveness (or, in the editor's case, be prepared to deal with the punishment).

More questions?

If anyone has questions like this, you can always post them on the Editor forum on MSDN, and they'll get answered. I'll answer questions like this one on this blog, though the answers will always be long and rambling, so use the forums if you appreciate brevity.

Comments

  • Anonymous
    May 04, 2010
    "the gist is that the C# language service was setting and unsetting read-only-ness on every step." I'm curious, why was the C# language service even touching this in the first place?

  • Anonymous
    May 04, 2010
    I think it was for edit and continue; they were protecting things from being editing while the debuggee was running, and only allowed editing when it was stopped.  It's been awhile, though, so that may not be completely accurate :-/

  • Anonymous
    May 11, 2010
    The comment has been removed

  • Anonymous
    July 07, 2010
    I was just trying the read-only regions on C# and I have a problem: context menus still work. That means, fo rinstance, that you can try a refactoring. Trying a refactoring will end up with VS crashing, btw. The problem with this is that if I have to implement a command filter, I'll be back in Orcas Extensibility Land. Is this a known bug?

  • Anonymous
    July 07, 2010
    @joj: If you find something that crashes VS, please make sure the Watson (crash report) gets submitted to Microsoft.  You can also file a bug on Connect (connect.microsoft.com/VisualStudio), if you want to see if/when the bug gets fixed. Context menus in general should still show up, as commands can still work in read-only regions (e.g. copy, go to definition, insert a breakpoint, etc.).  Individual commands that may fail should either disable themselves if it is cheap (it usually isn't, unfortunately) or fail gracefully (not crash). If the question is "is this specific crash known?", I wouldn't know.  If you file a Connect bug and include information about the language (C#) and commands that crash then you should get some answers. -Noah

  • Anonymous
    July 07, 2010
    Cool. Your explanation makes complete sense and I'll log the bug. Actually, it's two bugs as intellisense still shows, but does nothing (which is weird), and then there's the crash. Thanks again, joj.

  • Anonymous
    July 07, 2010
    Much appreciated!  I know it takes time to file these, so thank you for doing so. I'm guessing the one about intellisense showing up may be By Design'ed.  I know the fact that you can trigger intellisense even in read-only views is by design (people use it to inspect types), but how you trigger it may be a bug (i.e. if it triggers by "typing" a "." in a read-only region, that seems wrong, since you can't actually insert the ".").  That bug may also eventually get back to the editor team; I vaguely recall something like that being filed in the past, so command filters on the editor could tell when edits failed (and therefore not pop up intellsense).  I had a potential fix for it back at the end of Dev10, but it was a bit scary and a decent departure from backwards compatibility with VS2008, so we didn't take the fix.  Once you file it, the various feature teams (C# and the editor) can take a look to see if it is fixable this time around. Thanks! -Noah

  • Anonymous
    July 07, 2010
    No problem. What I haaaaaaaaaaate about Connect is the "Won't Fix" resolution. Lie to me and tell me you're delaying it to 2050, it sounds nicer. Anyway, if you want to follow the bug id is 573661 (in Visual Studio). I want to test a little more on the intellisense before submiting (it was pressing an "a", which doesn't make sense at all). Regards, joj.