diff --git a/PSReadLine/KeyBindings.vi.cs b/PSReadLine/KeyBindings.vi.cs
index 5a47c660..7af4a4fb 100644
--- a/PSReadLine/KeyBindings.vi.cs
+++ b/PSReadLine/KeyBindings.vi.cs
@@ -301,6 +301,8 @@ private void SetDefaultViBindings()
 
             _viChordTextObjectsTable = new Dictionary<PSKeyInfo, KeyHandler>
             {
+                { Keys.DQuote,          MakeKeyHandler(ViHandleTextObject,     "QuoteTextObject")},
+                { Keys.SQuote,          MakeKeyHandler(ViHandleTextObject,     "QuoteTextObject")},
                 { Keys.W,               MakeKeyHandler(ViHandleTextObject,     "WordTextObject")},
             };
             
diff --git a/PSReadLine/Position.cs b/PSReadLine/Position.cs
index 2aa32039..e4f492f4 100644
--- a/PSReadLine/Position.cs
+++ b/PSReadLine/Position.cs
@@ -10,19 +10,7 @@ public partial class PSConsoleReadLine
         /// </summary>
         /// <param name="current">The position in the current logical line.</param>
         private static int GetBeginningOfLinePos(int current)
-        {
-            int i = Math.Max(0, current);
-            while (i > 0)
-            {
-                if (_singleton._buffer[--i] == '\n')
-                {
-                    i += 1;
-                    break;
-                }
-            }
-
-            return i;
-        }
+            => _singleton._buffer.GetBeginningOfLogicalLinePos(current);
 
         /// <summary>
         /// Returns the position of the beginning of line
@@ -66,21 +54,7 @@ private static int GetBeginningOfNthLinePos(int lineIndex)
         /// <param name="current"></param>
         /// <returns></returns>
         private static int GetEndOfLogicalLinePos(int current)
-        {
-            var newCurrent = current;
-
-            for (var position = current; position < _singleton._buffer.Length; position++)
-            {
-                if (_singleton._buffer[position] == '\n')
-                {
-                    break;
-                }
-
-                newCurrent = position;
-            }
-
-            return newCurrent;
-        }
+            => _singleton._buffer.GetEndOfLogicalLinePos(current);
 
         /// <summary>
         /// Returns the position of the end of the logical line
diff --git a/PSReadLine/StringBuilderTextObjectExtensions.cs b/PSReadLine/StringBuilderTextObjectExtensions.cs
index 421ab345..00f82eb4 100644
--- a/PSReadLine/StringBuilderTextObjectExtensions.cs
+++ b/PSReadLine/StringBuilderTextObjectExtensions.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Management.Automation;
 using System.Text;
 
 namespace Microsoft.PowerShell
@@ -109,5 +110,161 @@ public static int ViFindBeginningOfNextWordObjectBoundary(this StringBuilder buf
             // Make sure end includes the starting position.
             return Math.Max(i, position);
         }
+
+        /// <summary>
+        /// Returns the span of text within the quotes relative to the specified position, in the corresponding logical line.
+        /// If the position refers to the given start delimiter, the method returns the position immediately.
+        /// If not, it first attempts to look backwards to find the start delimiter and returns its position if found.
+        /// Otherwise, it look forwards to find the start delimiter and returns its position if found.
+        /// Otherwise, it returns (-1, -1).
+        ///
+        /// If a start delimiter is found, this method then attempts to find the end delimiter within the logical line.
+        /// Otherwise, it returns (-1, -1).
+        /// 
+        /// This method supports VI i' and i" text objects.
+        /// </summary>
+        public static (int Start, int End) ViFindSpanOfInnerQuotedTextObjectBoundary(this StringBuilder buffer, char delimiter, int position, int repeated = 1)
+        {
+            // Cursor may be past the end of the buffer when calling this method
+            // this may happen if the cursor is at the beginning of a new line.
+
+            var pos = Math.Min(position, buffer.Length - 1);
+
+            // restrict this method to the logical line
+            // corresponding to the given position
+
+            var startOfLine = buffer.GetBeginningOfLogicalLinePos(pos);
+            var endOfLine = buffer.GetEndOfLogicalLinePos(pos);
+
+            var start = -1;
+            var end = -1;
+
+            // if on a quote we may be on a beginning or end quote
+            // we need to parse the line to find out
+
+            if (buffer[pos] == delimiter)
+            {
+                var count = 1;
+                for (var offset = pos - 1; offset > startOfLine; offset--)
+                {
+                    if (buffer[offset] == delimiter)
+                        count++;
+                }
+
+                // if there are an odd number of quotes up to the current position
+                // the position refers to the beginning a quoted text
+
+                if (count % 2 == 1)
+                {
+                    start = pos;
+                }
+            }
+
+            // else look backwards
+
+            if (start == -1)
+            {
+                for (var offset = pos - 1; offset > startOfLine; offset--)
+                {
+                    if (buffer[offset] == delimiter)
+                    {
+                        start = offset;
+                        break;
+                    }
+                }
+            }
+
+            // if not found, look forwards
+
+            if (start == -1)
+            {
+                for (var offset = pos; offset < endOfLine; offset++)
+                {
+                    if (buffer[offset] == delimiter)
+                    {
+                        start = offset;
+                        break;
+                    }
+                }
+            }
+
+            // attempts to find the end quote
+
+            if (start != -1 && start < endOfLine)
+            {
+                for (var offset = start + 1; offset < buffer.Length; offset++)
+                {
+                    if (buffer[offset] == delimiter)
+                    {
+                        end = offset;
+                        break;
+                    }
+                    if (buffer[offset] == '\n')
+                    {
+                        break;
+                    }
+                }
+            }
+
+            // adjust span boundaries based upon
+            // the number of repeatitions
+
+            if (start != -1 && end != -1)
+            {
+                if (repeated > 1)
+                {
+                    end++;
+                }
+                else
+                {
+                    start++;
+                }
+            }
+
+            return (start, end);
+        }
+
+        /// <summary>
+        /// Returns the position of the beginning of line
+        /// starting from the specified "current" position.
+        /// </summary>
+        /// <param name="current">The position in the current logical line.</param>
+        internal static int GetBeginningOfLogicalLinePos(this StringBuilder buffer, int current)
+        {
+            int i = Math.Max(0, current);
+            while (i > 0)
+            {
+                if (buffer[--i] == '\n')
+                {
+                    i += 1;
+                    break;
+                }
+            }
+
+            return i;
+        }
+
+        /// <summary>
+        /// Returns the position of the end of the logical line
+        /// as specified by the "current" position.
+        /// </summary>
+        /// <param name="current"></param>
+        /// <returns></returns>
+        internal static int GetEndOfLogicalLinePos(this StringBuilder buffer, int current)
+        {
+            var newCurrent = current;
+
+            for (var position = current; position < buffer.Length; position++)
+            {
+                if (buffer[position] == '\n')
+                {
+                    break;
+                }
+
+                newCurrent = position;
+            }
+
+            return newCurrent;
+        }
     }
 }
diff --git a/PSReadLine/TextObjects.Vi.cs b/PSReadLine/TextObjects.Vi.cs
index ea9810fb..b23c7538 100644
--- a/PSReadLine/TextObjects.Vi.cs
+++ b/PSReadLine/TextObjects.Vi.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
-
+using System.Runtime.CompilerServices;
+
 namespace Microsoft.PowerShell
 {
     public partial class PSConsoleReadLine
@@ -22,9 +23,17 @@ internal enum TextObjectSpan
         private TextObjectOperation _textObjectOperation = TextObjectOperation.None;
         private TextObjectSpan _textObjectSpan = TextObjectSpan.None;
 
-        private readonly Dictionary<TextObjectOperation, Dictionary<TextObjectSpan, KeyHandler>> _textObjectHandlers = new()
+        private readonly Dictionary<TextObjectOperation, Dictionary<TextObjectSpan, Dictionary<PSKeyInfo, KeyHandler>>> _textObjectHandlers = new()
         {
-            [TextObjectOperation.Delete] = new() { [TextObjectSpan.Inner] = MakeKeyHandler(ViDeleteInnerWord, "ViDeleteInnerWord") },
+            [TextObjectOperation.Delete] = new()
+            {
+                [TextObjectSpan.Inner] = new()
+                {
+                    [Keys.DQuote] = MakeKeyHandler(ViDeleteInnerDQuote, "ViDeleteInnerDQuote"),
+                    [Keys.SQuote] = MakeKeyHandler(ViDeleteInnerSQuote, "ViDeleteInnerSQuote"),
+                    [Keys.W] = MakeKeyHandler(ViDeleteInnerWord, "ViDeleteInnerWord"),
+                }
+            },
         };
 
         private void ViChordDeleteTextObject(ConsoleKeyInfo? key = null, object arg = null)
@@ -75,8 +84,12 @@ private TextObjectSpan GetRequestedTextObjectSpan(ConsoleKeyInfo key)
 
         private static void ViHandleTextObject(ConsoleKeyInfo? key = null, object arg = null)
         {
-            if (!_singleton._textObjectHandlers.TryGetValue(_singleton._textObjectOperation, out var textObjectHandler) ||
-                !textObjectHandler.TryGetValue(_singleton._textObjectSpan, out var handler))
+            System.Diagnostics.Debug.Assert(key != null);
+            var keyInfo = PSKeyInfo.FromConsoleKeyInfo(key.Value);
+
+            if (!_singleton._textObjectHandlers.TryGetValue(_singleton._textObjectOperation, out var textObjectSpanHandlers) ||
+                !textObjectSpanHandlers.TryGetValue(_singleton._textObjectSpan, out var textObjectKeyHandlers) ||
+                !textObjectKeyHandlers.TryGetValue(keyInfo, out var handler))
             {
                 ResetTextObjectState();
                 Ding();
@@ -92,6 +105,39 @@ private static void ResetTextObjectState()
             _singleton._textObjectSpan = TextObjectSpan.None;
         }
 
+        private static void ViDeleteInnerSQuote(ConsoleKeyInfo? key = null, object arg = null)
+            => ViDeleteInnerQuotes('\'', key, arg);
+        private static void ViDeleteInnerDQuote(ConsoleKeyInfo? key = null, object arg = null)
+            => ViDeleteInnerQuotes('\"', key, arg);
+
+        private static void ViDeleteInnerQuotes(char delimiter, ConsoleKeyInfo? key = null, object arg = null)
+        {
+            if (!TryGetArgAsInt(arg, out var numericArg, 1))
+            {
+                return;
+            }
+
+            if (_singleton._buffer.Length == 0)
+            {
+                Ding();
+                return;
+            }
+
+            var (start, end) = _singleton._buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, _singleton._current, repeated: numericArg);
+
+            if (start == -1 || end == -1)
+            {
+                Ding();
+                return;
+            }
+
+            var position = start;
+
+            _singleton.RemoveTextToViRegister(position, end - position);
+            _singleton.AdjustCursorPosition(position);
+            _singleton.Render();
+        }
+
         private static void ViDeleteInnerWord(ConsoleKeyInfo? key = null, object arg = null)
         {
             var delimiters = _singleton.Options.WordDelimiters;
diff --git a/test/StringBuilderTextObjectExtensionsTests.cs b/test/StringBuilderTextObjectExtensionsTests.cs
index 66bd590d..d7ec0dc6 100644
--- a/test/StringBuilderTextObjectExtensionsTests.cs
+++ b/test/StringBuilderTextObjectExtensionsTests.cs
@@ -73,5 +73,51 @@ public void StringBuilderTextObjectExtensions_ViFindBeginningOfNextWordObjectBou
             Assert.Equal(46, buffer.ViFindBeginningOfNextWordObjectBoundary(45, wordDelimiters));
             Assert.Equal(50, buffer.ViFindBeginningOfNextWordObjectBoundary(46, wordDelimiters));
         }
+
+        [Theory]
+        [InlineData('\'')]
+        [InlineData('\"')]
+        public void StringBuilderTextObjectExtensions_ViFindSpanOfInnerQuotedTextObjectBoundary(char delimiter)
+        {
+            var buffer = new StringBuilder($"_{delimiter}_{delimiter} {delimiter}_{delimiter} {delimiter}_{delimiter}");
+
+            // text:     _"_" "_" "_"
+            // position: 012345678901
+            //           -         1
+            // boundary: 111135557888
+
+            // when invoked once, the span is within the quotes
+
+            Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 0, repeated: 1));
+            Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 1, repeated: 1));
+            Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 2, repeated: 1));
+            Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 3, repeated: 1));
+            Assert.Equal((4, 5), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 4, repeated: 1));
+            Assert.Equal((6, 7), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 5, repeated: 1));
+            Assert.Equal((6, 7), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 6, repeated: 1));
+            Assert.Equal((6, 7), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 7, repeated: 1));
+            Assert.Equal((8, 9), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 8, repeated: 1));
+            Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 9, repeated: 1));
+            Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 10, repeated: 1));
+            Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 11, repeated: 1));
+            Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 12, repeated: 1));
+
+            // when invoked more than once, the span is around the quotes
+
+            Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 0, repeated: 42));
+            Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 1, repeated: 42));
+            Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 2, repeated: 42));
+            Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 3, repeated: 42));
+            Assert.Equal((3, 6), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 4, repeated: 42));
+            Assert.Equal((5, 8), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 5, repeated: 42));
+            Assert.Equal((5, 8), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 6, repeated: 42));
+            Assert.Equal((5, 8), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 7, repeated: 42));
+            Assert.Equal((7, 10), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 8, repeated: 42));
+            Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 9, repeated: 42));
+            Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 10, repeated: 42));
+            Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 11, repeated: 42));
+            Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 12, repeated: 42));
+
+        }
     }
 }
diff --git a/test/TextObjects.Vi.diquotes.Tests.cs b/test/TextObjects.Vi.diquotes.Tests.cs
new file mode 100644
index 00000000..b413ad54
--- /dev/null
+++ b/test/TextObjects.Vi.diquotes.Tests.cs
@@ -0,0 +1,89 @@
+using Xunit;
+
+namespace Test
+{
+    public partial class ReadLine
+    {
+        [SkippableTheory]
+        [InlineData("a 'quoted' text", "di'", "a '' text")]
+        [InlineData("a \"quoted\" text", "di\"", "a \"\" text")]
+        public void ViTextObject_diquotes(string input, string motion, string expected)
+        {
+            TestSetup(KeyMode.Vi);
+
+            Test(expected, Keys(
+                input, _.Escape,
+                "Fo", // move backwards to the letter 'o' inside the quotes
+
+                // delete text object (delete inner quotes)
+                // this will find the surrounding quotes at the position
+
+                motion,
+
+                CheckThat(() => AssertCursorLeftIs(3))
+            ));
+
+            Test(expected, Keys(
+                input, _.Escape,
+                "0", // move to beginning of logical line
+
+                // delete text object (delete inner quotes)
+                // this will look forward and find the quoted text
+
+                motion,
+
+                CheckThat(() => AssertCursorLeftIs(3))
+            ));
+        }
+
+        [SkippableTheory]
+        [InlineData("a 'quoted' text", "42di'", "a  text")]
+        [InlineData("a \"quoted\" text", "42di\"", "a  text")]
+        public void ViTextObject_diquotes_argument(string input, string motion, string expected)
+        {
+            TestSetup(KeyMode.Vi);
+
+            Test(expected, Keys(
+                input, _.Escape,
+                "Fo", // move backwards to the letter 'o' inside the quotes
+
+                // delete text object (delete inner quotes)
+                // no matter how many times, we only care about more than once or not
+
+                motion,
+                CheckThat(() => AssertCursorLeftIs(2))
+            ));
+        }
+
+        [SkippableTheory]
+        [InlineData("an 'incorrectly quoted text", "di'", "A'", "an 'incorrectly quoted text'")]
+        [InlineData("an \"incorrectly quoted text", "di\"", "A\"", "an \"incorrectly quoted text\"")]
+        public void ViTextObject_diquotes_noop(string input, string motion, string fix, string expected)
+        {
+            TestSetup(KeyMode.Vi);
+
+            TestMustDing("a text", Keys(
+                "a text", _.Escape,
+
+                // attempt to delete non-existent text object must ding
+                // no matter how many times, we only care about more than once or not
+
+                motion
+            ));
+
+            TestMustDing(expected, Keys(
+                input, _.Escape,
+
+                // even though there is a starting delimiter, 
+                // the motion cannot find an ending delimiter
+
+                motion,
+
+                // we must make the input a valid line
+                // move to the end of the buffer and add the missing quote
+
+                fix
+            ));
+        }
+    }
+}
diff --git a/test/TextObjects.Vi.Tests.cs b/test/TextObjects.Vi.diword.Tests.cs
similarity index 100%
rename from test/TextObjects.Vi.Tests.cs
rename to test/TextObjects.Vi.diword.Tests.cs