This commit is contained in:
Mikayla Hutchinson 2019-09-27 18:59:10 -04:00
Родитель 5f73e5bcd5
Коммит a2903de9e1
10 изменённых файлов: 171 добавлений и 164 удалений

Просмотреть файл

@ -53,16 +53,6 @@ namespace MonoDevelop.Xml.Dom
}
}
public IEnumerable<XObject> SelfAndParents {
get {
var next = this;
while (next != null) {
yield return next;
next = next.Parent;
}
}
}
public TextSpan Span { get; protected set; }
public void End (int offset)

Просмотреть файл

@ -57,8 +57,8 @@ namespace MonoDevelop.Xml.Parser
// so attach a node to the DOM and end the state
var comment = (XComment) context.Nodes.Pop ();
comment.End (context.Position + 1);
if (context.BuildTree) {
comment.End (context.Position + 1);
((XContainer) context.Nodes.Peek ()).AddChildNode (comment);
}

Просмотреть файл

@ -224,5 +224,6 @@ namespace MonoDevelop.Xml.Parser
internal static bool MaybeDocType (XmlParser parser) => parser.CurrentState is XmlRootState && parser.GetContext ().StateTag == DOCTYPE;
internal static bool MaybeComment (XmlParser parser) => parser.CurrentState is XmlRootState && parser.GetContext ().StateTag == COMMENT;
internal static bool MaybeCDataOrCommentOrDocType (XmlParser parser) => parser.CurrentState is XmlRootState && parser.GetContext ().StateTag == BRACKET_EXCLAM;
public static bool IsNotFree (XmlParser parser) => parser.CurrentState is XmlRootState && parser.GetContext ().StateTag != FREE;
}
}

Просмотреть файл

@ -58,8 +58,7 @@ namespace MonoDevelop.Xml.Editor.Completion
// if we're completing an existing element, remove it from the path
// so we don't get completions for its children instead
if (nodePath.Count > 0) {
var lastNode = nodePath[nodePath.Count - 1] as XElement;
if (lastNode != null && lastNode.Name.Length == applicableToSpan.Length) {
if (nodePath[nodePath.Count-1] is XElement leaf && leaf.Name.Length == applicableToSpan.Length) {
nodePath.RemoveAt (nodePath.Count - 1);
}
}

Просмотреть файл

@ -56,16 +56,17 @@ namespace MonoDevelop.Xml.Editor.TextStructure
XmlParser spine = null;
if (lastParse != null && lastParse.TextSnapshot.Version.VersionNumber == activeSpan.Snapshot.Version.VersionNumber) {
var n = lastParse.XDocument.FindAtOrBeforeOffset (activeSpan.Start.Position);
nodePath = n.SelfAndParents.ToList ();
nodePath = n.GetPath ();
} else {
spine = parser.GetSpineParser (activeSpan.End);
nodePath = spine.GetNodePathWithCompleteLeafElement (activeSpan.Snapshot);
//put spine parser in tree parser mode so it connects element closing nodes
spine = parser.GetSpineParser (activeSpan.Start).GetTreeParser ();
nodePath = spine.AdvanceToNodeEndAndGetNodePath (activeSpan.Snapshot);
}
// this is a little odd because it was ported from MonoDevelop, where it has to maintain its own stack of state
// for contract selection. it describes the current semantic selection as a node path, the index of the node in that path
// that's selected, and the kind of selection that node has.
int selectedNodeIndex = -1;
int selectedNodeIndex = nodePath.Count;
SelectionLevel selectionLevel = default;
// keep on expanding the selection until we find one that contains the current selection but is a little bigger
@ -91,7 +92,7 @@ namespace MonoDevelop.Xml.Editor.TextStructure
TextSpan? GetSelectionSpan (ITextSnapshot snapshot, List<XObject> nodePath, ref int index, ref SelectionLevel level)
{
if (index < 0) {
if (index < 0 || index >= nodePath.Count) {
return null;
}
var current = nodePath[index];
@ -124,16 +125,19 @@ namespace MonoDevelop.Xml.Editor.TextStructure
bool ExpandSelection (List<XObject> nodePath, XmlParser spine, SnapshotSpan activeSpan, ref int index, ref SelectionLevel level)
{
if (index + 1 == nodePath.Count) {
if (index - 1 < 0) {
return false;
}
//if an index is selected, we may need to transition level rather than transitioning index
if (index >= 0) {
if (index < nodePath.Count) {
var current = nodePath[index];
if (current is XElement element) {
switch (level) {
case SelectionLevel.Self:
if (spine != null && !spine.AdvanceUntilClosed (element, activeSpan.Snapshot, 5000)) {
return false;
}
if (!element.IsSelfClosing) {
level = SelectionLevel.OuterElement;
return true;
@ -165,7 +169,7 @@ namespace MonoDevelop.Xml.Editor.TextStructure
}
//advance up the node path
index++;
index--;
var newNode = nodePath[index];
//determine the starting selection level for the new node
@ -174,7 +178,7 @@ namespace MonoDevelop.Xml.Editor.TextStructure
return true;
}
if (spine != null && !spine.AdvanceUntilClosed (newNode, activeSpan.Snapshot, 5000)) {
if (spine != null && !spine.AdvanceUntilEnded (newNode, activeSpan.Snapshot, 5000)) {
return false;
}
@ -206,6 +210,10 @@ namespace MonoDevelop.Xml.Editor.TextStructure
return true;
}
if (spine != null && !spine.AdvanceUntilClosed (newNode, activeSpan.Snapshot, 5000)) {
return false;
}
if (newNode is XElement el && el.ClosingTag != null) {
if (el.IsSelfClosing) {
level = SelectionLevel.Self;

Просмотреть файл

@ -1,9 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Text;
using MonoDevelop.Xml.Dom;
using MonoDevelop.Xml.Parser;
@ -12,6 +12,8 @@ namespace MonoDevelop.Xml.Editor
{
public static class XmlParserExtensions
{
const int DEFAULT_READAHEAD_LIMIT = 5000;
/// <summary>
/// Gets the XML name at the parser's position.
/// </summary>
@ -78,8 +80,12 @@ namespace MonoDevelop.Xml.Editor
/// <param name="snapshot">The text snapshot corresponding to the parser.</param>
/// <param name="maximumReadahead">Maximum number of characters to advance before giving up.</param>
/// <returns>Whether the object was successfully completed</returns>
public static bool AdvanceUntilClosed (this XmlParser parser, XObject ob, ITextSnapshot snapshot, int maximumReadahead = 500)
public static bool AdvanceUntilClosed (this XmlParser parser, XObject ob, ITextSnapshot snapshot, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
{
if (!parser.GetContext().BuildTree) {
throw new ArgumentException ("Parser must be in tree mode");
}
var el = ob as XElement;
if (el == null) {
return AdvanceUntilEnded (parser, ob, snapshot, maximumReadahead);
@ -93,7 +99,8 @@ namespace MonoDevelop.Xml.Editor
if (el.IsClosed) {
return true;
}
if (parser.Nodes.Count < startingDepth) {
// just in case, bail if we pop out past the element's parent
if (parser.Nodes.Count < startingDepth - 1) {
return false;
}
}
@ -109,7 +116,7 @@ namespace MonoDevelop.Xml.Editor
/// <param name="snapshot">The text snapshot corresponding to the parser.</param>
/// <param name="maximumReadahead">Maximum number of characters to advance before giving up.</param>
/// <returns>Whether the object was successfully completed</returns>
public static bool AdvanceUntilEnded (this XmlParser parser, XObject ob, ITextSnapshot snapshot, int maximumReadahead = 500)
public static bool AdvanceUntilEnded (this XmlParser parser, XObject ob, ITextSnapshot snapshot, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
{
var startingDepth = parser.Nodes.Count;
@ -127,97 +134,98 @@ namespace MonoDevelop.Xml.Editor
}
/// <summary>
/// Gets the node path at the parser condition. Reads ahead to complete names, but does not complete the nodes.
/// Gets the node path at the parser position without changing the parser state, ensuring that the deepest node has a complete name.
/// </summary>
/// <param name="parser">A spine parser. Its state will be modified.</param>
/// <param name="parser">A spine parser. Its state will not be modified.</param>
/// <param name="snapshot">The text snapshot corresponding to the parser.</param>
public static List<XObject> GetNodePath (this XmlParser spine, ITextSnapshot snapshot)
{
var path = new List<XObject> (spine.Nodes);
var path = spine.Nodes.ToNodePath ();
//remove the root XDocument
path.RemoveAt (path.Count - 1);
//complete incomplete XName if present
if (spine.CurrentState is XmlNameState && path[0] is INamedXObject) {
path[0] = path[0].ShallowCopy ();
//complete last node's name without altering the parser state
int lastIdx = path.Count - 1;
if (spine.CurrentState is XmlNameState && path[lastIdx] is INamedXObject) {
XName completeName = GetCompleteName (spine, snapshot);
((INamedXObject)path[0]).Name = completeName;
var obj = path[lastIdx] = path[lastIdx].ShallowCopy ();
((INamedXObject)obj).Name = completeName;
}
return path;
}
/// <summary>
/// Advances the parser to end the node at the current position and gets that node's path.
/// </summary>
/// <param name="spine">A spine parser. Its state will be modified.</param>
/// <param name="snapshot">The text snapshot corresponding to the parser.</param>
/// <returns></returns>
public static List<XObject> AdvanceToNodeEndAndGetNodePath (this XmlParser spine, ITextSnapshot snapshot, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
{
if (!spine.GetContext ().BuildTree) {
throw new ArgumentException ("Parser must be in tree mode");
}
int startOffset = spine.Position;
int startDepth = spine.Nodes.Count;
//if in potential start of a state, advance into the next state
var end = Math.Min (snapshot.Length - spine.Position, maximumReadahead) + spine.Position;
if (spine.Position < end && (XmlRootState.IsNotFree (spine) || (spine.CurrentState is XmlRootState && snapshot[spine.Position] == '<'))) {
do {
spine.Push (snapshot[spine.Position]);
} while (spine.Position < end && XmlRootState.IsNotFree (spine));
//if it transitioned to another state, eat until we get a new node on the stack
if (spine.Position < end && !(spine.CurrentState is XmlRootState) && spine.Nodes.Count <= startDepth) {
spine.Push (snapshot[spine.Position]);
}
}
var path = spine.Nodes.ToNodePath ();
// make sure the leaf node is ended
if (path.Count > 0) {
var leaf = path[path.Count-1];
if (!(leaf is XDocument)) {
AdvanceUntilEnded (spine, leaf, snapshot, maximumReadahead - (spine.Position - startOffset));
}
//the leaf node might have a child that's a better match for the offset
if (leaf is XContainer c && c.FindAtOffset (startOffset) is XObject o) {
path.Add (o);
}
}
return path;
}
static List<XObject> ToNodePath (this NodeStack stack)
{
var path = new List<XObject> (stack);
path.Reverse ();
return path;
}
public static List<XObject> GetPath (this XObject obj)
{
var path = new List<XObject> ();
while (obj != null) {
path.Add (obj);
obj = obj.Parent;
}
path.Reverse ();
return path;
}
/// <summary>
/// Gets the node path at the parser condition, ensuring that the deepest element is closed.
/// </summary>
/// <param name="parser">A spine parser. Its state will be modified.</param>
/// <param name="snapshot">The text snapshot corresponding to the parser.</param>
/// <returns></returns>
public static List<XObject> GetNodePathWithCompleteLeafElement (this XmlParser parser, ITextSnapshot snapshot)
public static void ConnectParents (this List<XObject> nodePath)
{
int offset = parser.Position;
var length = snapshot.Length;
int i = offset;
var nodePath = parser.Nodes.ToList ();
//if inside body of unclosed element, capture whole body
if (parser.CurrentState is XmlRootState && parser.Nodes.Peek () is XElement unclosedEl) {
while (i < length && InRootOrClosingTagState () && !unclosedEl.IsClosed) {
parser.Push (snapshot[i++]);
}
}
//if in potential start of a state, capture it
else if (parser.CurrentState is XmlRootState && GetStateTag () > 0) {
//eat until we figure out whether it's a state transition
while (i < length && GetStateTag () > 0) {
parser.Push (snapshot[i++]);
}
//if it transitioned to another state, eat until we get a new node on the stack
if (NotInRootState ()) {
var newState = parser.CurrentState;
while (i < length && NotInRootState () && parser.Nodes.Count <= nodePath.Count) {
parser.Push (snapshot[i++]);
}
if (parser.Nodes.Count > nodePath.Count) {
nodePath.Insert (0, parser.Nodes.Peek ());
}
}
}
//ensure any unfinished names are captured
while (i < length && InNameOrAttributeState ()) {
parser.Push (snapshot[i++]);
}
//if nodes are incomplete, they won't get connected
if (nodePath.Count > 1) {
for (int idx = 0; idx < nodePath.Count - 1; idx++) {
var node = nodePath[idx];
if (node.Parent == null) {
var parent = nodePath[idx + 1];
node.Parent = parent;
}
var parent = nodePath[nodePath.Count - 1];
for (int i = nodePath.Count - 2; i >= 0; i--) {
var node = nodePath[i];
node.Parent = parent;
parent = node;
}
}
return nodePath;
bool InNameOrAttributeState () =>
parser.CurrentState is XmlNameState
|| parser.CurrentState is XmlAttributeState
|| parser.CurrentState is XmlAttributeValueState;
bool InRootOrClosingTagState () =>
parser.CurrentState is XmlRootState
|| parser.CurrentState is XmlNameState
|| parser.CurrentState is XmlClosingTagState;
int GetStateTag () => ((IXmlParserContext)parser).StateTag;
bool NotInRootState () => !(parser.CurrentState is XmlRootState);
}
public static string GetIncompleteValue (this XmlParser spineAtCaret, ITextSnapshot snapshot)

Просмотреть файл

@ -56,5 +56,8 @@ namespace MonoDevelop.Xml.Tests.Completion
public IEditorCommandHandlerServiceFactory CommandServiceFactory
=> Host.GetService<IEditorCommandHandlerServiceFactory> ();
public ITextStructureNavigatorSelectorService TextStructureNavigatorSelectorService
=> Host.GetService<ITextStructureNavigatorSelectorService> ();
}
}

Просмотреть файл

@ -38,13 +38,18 @@ namespace MonoDevelop.Xml.Tests.EditorTestHelpers
public virtual ITextView CreateTextView (string documentText, string filename = null)
{
var buffer = Catalog.BufferFactoryService.CreateTextBuffer (documentText, ContentType);
var buffer = CreateTextBuffer (documentText);
if (filename != null) {
Catalog.TextDocumentFactoryService.CreateTextDocument (buffer, filename);
}
return Catalog.TextViewFactory.CreateTextView (buffer);
}
public virtual ITextBuffer CreateTextBuffer (string documentText)
{
return Catalog.BufferFactoryService.CreateTextBuffer (documentText, ContentType);
}
protected (string document, int caretOffset) ExtractCaret (string document, char caretMarkerChar)
{
var caretOffset = document.IndexOf (caretMarkerChar);

Просмотреть файл

@ -25,24 +25,25 @@
// THE SOFTWARE.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MonoDevelop.Ide;
using MonoDevelop.Ide.Editor;
using MonoDevelop.Ide.Editor.Extension;
using Microsoft.VisualStudio.MiniEditor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using MonoDevelop.Xml.Editor;
using MonoDevelop.Xml.Editor.Completion;
using MonoDevelop.Xml.Tests.Completion;
using MonoDevelop.Xml.Tests.EditorTestHelpers;
using NUnit.Framework;
namespace MonoDevelop.Xml.Tests
{
[TestFixture]
public class ExpandSelectionTests : TextEditorExtensionTestBase
public class ExpandSelectionTests : EditorTestBase
{
public static EditorExtensionTestData XmlContentData = new EditorExtensionTestData (
fileName: "/a.xml",
language: "C#",
mimeType: "application/xml",
projectFileName: "test.csproj"
);
protected override string ContentTypeName => XmlContentTypeNames.XmlCore;
protected override (EditorEnvironment, EditorCatalog) InitializeEnvironment () => TestEnvironment.EnsureInitialized ();
const string Document = @"<!-- this is
a comment-->
@ -65,6 +66,8 @@ a comment-->";
</bar>
</foo>";
const string TextNode = "this is some text";
const string AttributesFoo = @"hello=""hi"" goodbye=""bye""";
const string AttributeHello = @"hello=""hi""";
@ -93,13 +96,6 @@ a comment-->";
const string CommentBar = @"<!--another comment-->";
protected override EditorExtensionTestData GetContentData () => XmlContentData;
protected override IEnumerable<TextEditorExtension> GetEditorExtensions ()
{
yield return new XmlTextEditorExtension ();
}
//args are line, col, then the expected sequence of expansions
[Test]
[TestCase (1, 2, CommentDoc)]
@ -107,54 +103,53 @@ a comment-->";
[TestCase (3, 3, "foo", ElementFoo, ElementWithBodyFoo)]
[TestCase (3, 15, "hi", AttributeHello, AttributesFoo, ElementFoo, ElementWithBodyFoo)]
[TestCase (3, 7, "hello", AttributeHello, AttributesFoo, ElementFoo, ElementWithBodyFoo)]
[TestCase (4, 7, BodyFoo, ElementWithBodyFoo)]
[TestCase (4, 7, TextNode, BodyFoo, ElementWithBodyFoo)]
[TestCase (5, 22, "done", AttributeThing, ElementBaz, BodyBar, ElementWithBodyBar, BodyFoo, ElementWithBodyFoo)]
[TestCase (6, 12, CommentBar, BodyBar, ElementWithBodyBar, BodyFoo, ElementWithBodyFoo)]
public async Task TestExpandShrink (object[] args)
{
var loc = new DocumentLocation ((int)args [0], (int)args[1]);
using (var testCase = await SetupTestCase (Document)) {
var doc = testCase.Document;
doc.Editor.SetCaretLocation (loc);
var ext = doc.GetContent<BaseXmlEditorExtension> ();
var buffer = CreateTextBuffer (Document);
var parser = XmlBackgroundParser.GetParser<XmlBackgroundParser> ((ITextBuffer2)buffer);
var snapshot = buffer.CurrentSnapshot;
var navigator = Catalog.TextStructureNavigatorSelectorService.GetTextStructureNavigator (buffer);
var line = snapshot.GetLineFromLineNumber ((int)args[0] - 1);
var offset = line.Start + (int)args[1] - 1;
//check initial state
Assert.IsFalse (doc.Editor.IsSomethingSelected);
Assert.AreEqual (loc, doc.Editor.CaretLocation);
// it's extremely unlikely the parser will hve an up to date parse result yet
// so this should use the spine parser codepath
//check expanding causes correct selections
for (int i = 2; i < args.Length; i++) {
ext.ExpandSelection ();
Assert.AreEqual (args [i], doc.Editor.SelectedText);
}
SnapshotSpan Span(int s, int l) => new SnapshotSpan (snapshot, s, l);
//check entire doc is selected
ext.ExpandSelection ();
var sel = doc.Editor.SelectionRange;
Assert.AreEqual (0, sel.Offset);
Assert.AreEqual (Document.Length, sel.Length);
var span = Span (offset, 0);
//check expanding again does not change it
ext.ExpandSelection ();
Assert.AreEqual (0, sel.Offset);
Assert.AreEqual (Document.Length, sel.Length);
//check shrinking causes correct selections
for (int i = args.Length - 1; i >= 2; i--) {
ext.ShrinkSelection ();
Assert.AreEqual (args [i], doc.Editor.SelectedText);
}
//final shrink back to a caret
ext.ShrinkSelection ();
Assert.IsFalse (doc.Editor.IsSomethingSelected);
Assert.AreEqual (loc, doc.Editor.CaretLocation);
//check shrinking again does not change it
ext.ShrinkSelection ();
Assert.IsFalse (doc.Editor.IsSomethingSelected);
Assert.AreEqual (loc, doc.Editor.CaretLocation);
//check expanding causes correct selections
for (int i = 2; i < args.Length; i++) {
span = navigator.GetSpanOfEnclosing (span);
var text = snapshot.GetText (span);
Assert.AreEqual (args[i], text);
}
//check entire doc is selected
span = navigator.GetSpanOfEnclosing (span);
Assert.AreEqual (0, span.Start.Position);
Assert.AreEqual (snapshot.Length, span.Length);
// now repeat the tests with an up to date parse result
await parser.GetOrParseAsync (snapshot, CancellationToken.None);
span = Span (offset, 0);
//check expanding causes correct selections
for (int i = 2; i < args.Length; i++) {
span = navigator.GetSpanOfEnclosing (span);
var text = snapshot.GetText (span);
Assert.AreEqual (args[i], text);
}
//check entire doc is selected
span = navigator.GetSpanOfEnclosing (span);
Assert.AreEqual (0, span.Start.Position);
Assert.AreEqual (snapshot.Length, span.Length);
}
}
}

Просмотреть файл

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
@ -13,7 +13,6 @@
<ProjectReference Include="..\external\MiniEditor\Microsoft.VisualStudio.MiniEditor\Microsoft.VisualStudio.MiniEditor.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="ExpandSelectionTests.cs" />
<Compile Remove="Schema\SchemaAssociationTests.cs" />
<Compile Remove="Schema\XmlSchemaNamespaceTests.cs" />
</ItemGroup>
@ -26,7 +25,6 @@
<EmbeddedResource Include="Schema\XMLSchema.xsd" LogicalName="XMLSchema.xsd" />
</ItemGroup>
<ItemGroup>
<None Include="ExpandSelectionTests.cs" />
<None Include="Schema\SchemaAssociationTests.cs" />
<None Include="Schema\XmlSchemaNamespaceTests.cs" />
</ItemGroup>