From 8bee4caf5336f385b2c6e95c5aec7c39cac4d5b8 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Fri, 28 Nov 2025 12:48:12 -0700 Subject: [PATCH 1/6] Add double-click selection for N-prefixed strings --- .../Modules/KeypressCommandFilter.cs | 105 +++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/AxialSqlTools/Modules/KeypressCommandFilter.cs b/AxialSqlTools/Modules/KeypressCommandFilter.cs index 981dae7..55d44ec 100644 --- a/AxialSqlTools/Modules/KeypressCommandFilter.cs +++ b/AxialSqlTools/Modules/KeypressCommandFilter.cs @@ -11,9 +11,10 @@ namespace AxialSqlTools { - public class KeypressCommandFilter : IOleCommandTarget + public class KeypressCommandFilter : IOleCommandTarget, IVsTextViewFilter { private IOleCommandTarget nextCommandTarget; + private IVsTextViewFilter nextTextViewFilter; private IVsTextView textView; private AxialSqlToolsPackage package; @@ -30,6 +31,8 @@ public void AddToChain() { throw new Exception("Failed to add command filter"); } + + nextTextViewFilter = nextCommandTarget as IVsTextViewFilter; } public int Exec(ref Guid cmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) @@ -131,5 +134,105 @@ public int QueryStatus(ref Guid cmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr p return nextCommandTarget?.QueryStatus(ref cmdGroup, cCmds, prgCmds, pCmdText) ?? VSConstants.S_OK; } + + public int GetWordExtent(int iLine, int iIdx, uint dwFlags, TextSpan[] pSpan) + { + if (pSpan == null || pSpan.Length == 0) + { + return nextTextViewFilter?.GetWordExtent(iLine, iIdx, dwFlags, pSpan) ?? VSConstants.E_FAIL; + } + + bool isDoubleClick = (dwFlags & (uint)WORDEXTFLAGS.WORDEXT_DBLCLICK) != 0; + + if (isDoubleClick && textView.GetBuffer(out IVsTextLines textLines) == VSConstants.S_OK) + { + textLines.GetLengthOfLine(iLine, out int iLength); + textLines.GetLineText(iLine, 0, iLine, iLength, out string lineText); + + if (TryGetQuotedStringSpan(lineText, iIdx, out int startIndex, out int endIndex)) + { + pSpan[0].iStartLine = iLine; + pSpan[0].iStartIndex = startIndex; + pSpan[0].iEndLine = iLine; + pSpan[0].iEndIndex = endIndex + 1; // TextSpan end index is exclusive + + return VSConstants.S_OK; + } + } + + return nextTextViewFilter?.GetWordExtent(iLine, iIdx, dwFlags, pSpan) ?? VSConstants.E_FAIL; + } + + private static bool TryGetQuotedStringSpan(string lineText, int position, out int startIndex, out int endIndex) + { + startIndex = -1; + endIndex = -1; + + if (string.IsNullOrEmpty(lineText) || position < 0 || position > lineText.Length) + { + return false; + } + + int searchIndex = 0; + + while (searchIndex < lineText.Length) + { + int openingQuote = lineText.IndexOf('\'', searchIndex); + + if (openingQuote == -1) + { + break; + } + + int closingQuote = FindClosingQuote(lineText, openingQuote + 1); + + if (closingQuote == -1) + { + break; + } + + if (position >= openingQuote && position <= closingQuote) + { + startIndex = openingQuote; + endIndex = closingQuote; + + if (openingQuote > 0 && (lineText[openingQuote - 1] == 'N' || lineText[openingQuote - 1] == 'n')) + { + startIndex--; + } + + return true; + } + + searchIndex = closingQuote + 1; + } + + return false; + } + + private static int FindClosingQuote(string lineText, int startIndex) + { + int index = startIndex; + + while (index < lineText.Length) + { + int quoteIndex = lineText.IndexOf('\'', index); + + if (quoteIndex == -1) + { + return -1; + } + + if (quoteIndex + 1 < lineText.Length && lineText[quoteIndex + 1] == '\'') + { + index = quoteIndex + 2; // Skip escaped quotes + continue; + } + + return quoteIndex; + } + + return -1; + } } } From 300826a7462a3d838cbac27fe49bbe423a9e47cc Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Fri, 28 Nov 2025 12:51:49 -0700 Subject: [PATCH 2/6] Implement remaining IVsTextViewFilter members --- .../Modules/KeypressCommandFilter.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/AxialSqlTools/Modules/KeypressCommandFilter.cs b/AxialSqlTools/Modules/KeypressCommandFilter.cs index 55d44ec..02ac493 100644 --- a/AxialSqlTools/Modules/KeypressCommandFilter.cs +++ b/AxialSqlTools/Modules/KeypressCommandFilter.cs @@ -163,6 +163,28 @@ public int GetWordExtent(int iLine, int iIdx, uint dwFlags, TextSpan[] pSpan) return nextTextViewFilter?.GetWordExtent(iLine, iIdx, dwFlags, pSpan) ?? VSConstants.E_FAIL; } + public int GetPairExtents(int iLine, int iIndex, TextSpan[] pts) + { + if (nextTextViewFilter != null) + { + return nextTextViewFilter.GetPairExtents(iLine, iIndex, pts); + } + + return VSConstants.E_NOTIMPL; + } + + public int GetDataTipText(TextSpan[] pSpan, out string pbstrText) + { + pbstrText = null; + + if (nextTextViewFilter != null) + { + return nextTextViewFilter.GetDataTipText(pSpan, out pbstrText); + } + + return VSConstants.E_NOTIMPL; + } + private static bool TryGetQuotedStringSpan(string lineText, int position, out int startIndex, out int endIndex) { startIndex = -1; From 11d01b420387255a7d11843be7eeffabc7f013ae Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Fri, 28 Nov 2025 12:56:40 -0700 Subject: [PATCH 3/6] Handle word extent requests without WORDEXT_DBLCLICK --- AxialSqlTools/Modules/KeypressCommandFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AxialSqlTools/Modules/KeypressCommandFilter.cs b/AxialSqlTools/Modules/KeypressCommandFilter.cs index 02ac493..5e08925 100644 --- a/AxialSqlTools/Modules/KeypressCommandFilter.cs +++ b/AxialSqlTools/Modules/KeypressCommandFilter.cs @@ -142,7 +142,7 @@ public int GetWordExtent(int iLine, int iIdx, uint dwFlags, TextSpan[] pSpan) return nextTextViewFilter?.GetWordExtent(iLine, iIdx, dwFlags, pSpan) ?? VSConstants.E_FAIL; } - bool isDoubleClick = (dwFlags & (uint)WORDEXTFLAGS.WORDEXT_DBLCLICK) != 0; + bool isDoubleClick = (WORDEXTFLAGS)dwFlags == WORDEXTFLAGS.WORDEXT_FINDWORD; if (isDoubleClick && textView.GetBuffer(out IVsTextLines textLines) == VSConstants.S_OK) { From 5ec157e5c57e8c9b11dc51baae2d43c6340ce7f5 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Fri, 28 Nov 2025 13:07:38 -0700 Subject: [PATCH 4/6] Register text view filter for word extents --- AxialSqlTools/Modules/KeypressCommandFilter.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/AxialSqlTools/Modules/KeypressCommandFilter.cs b/AxialSqlTools/Modules/KeypressCommandFilter.cs index 5e08925..f332b66 100644 --- a/AxialSqlTools/Modules/KeypressCommandFilter.cs +++ b/AxialSqlTools/Modules/KeypressCommandFilter.cs @@ -32,7 +32,14 @@ public void AddToChain() throw new Exception("Failed to add command filter"); } - nextTextViewFilter = nextCommandTarget as IVsTextViewFilter; + if (textView != null && textView.AddTextViewFilter(this, out IVsTextViewFilter downstreamFilter) == VSConstants.S_OK) + { + nextTextViewFilter = downstreamFilter; + } + else + { + nextTextViewFilter = nextCommandTarget as IVsTextViewFilter; + } } public int Exec(ref Guid cmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) From 6dadf60c03bb6a1134ef93282b422cf2d093e21c Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Fri, 28 Nov 2025 13:18:09 -0700 Subject: [PATCH 5/6] Implement mouse processor for SQL string selection --- AxialSqlTools/AxialSqlTools.csproj | 1 + .../Modules/KeypressCommandFilter.cs | 138 +---------------- .../SqlStringSelectionMouseProcessor.cs | 143 ++++++++++++++++++ 3 files changed, 145 insertions(+), 137 deletions(-) create mode 100644 AxialSqlTools/Modules/SqlStringSelectionMouseProcessor.cs diff --git a/AxialSqlTools/AxialSqlTools.csproj b/AxialSqlTools/AxialSqlTools.csproj index f5510d2..54ecaaa 100644 --- a/AxialSqlTools/AxialSqlTools.csproj +++ b/AxialSqlTools/AxialSqlTools.csproj @@ -113,6 +113,7 @@ + diff --git a/AxialSqlTools/Modules/KeypressCommandFilter.cs b/AxialSqlTools/Modules/KeypressCommandFilter.cs index f332b66..8890618 100644 --- a/AxialSqlTools/Modules/KeypressCommandFilter.cs +++ b/AxialSqlTools/Modules/KeypressCommandFilter.cs @@ -2,19 +2,14 @@ using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio.TextManager.Interop; using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; using System.Windows.Input; namespace AxialSqlTools { - public class KeypressCommandFilter : IOleCommandTarget, IVsTextViewFilter + public class KeypressCommandFilter : IOleCommandTarget { private IOleCommandTarget nextCommandTarget; - private IVsTextViewFilter nextTextViewFilter; private IVsTextView textView; private AxialSqlToolsPackage package; @@ -31,15 +26,6 @@ public void AddToChain() { throw new Exception("Failed to add command filter"); } - - if (textView != null && textView.AddTextViewFilter(this, out IVsTextViewFilter downstreamFilter) == VSConstants.S_OK) - { - nextTextViewFilter = downstreamFilter; - } - else - { - nextTextViewFilter = nextCommandTarget as IVsTextViewFilter; - } } public int Exec(ref Guid cmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) @@ -141,127 +127,5 @@ public int QueryStatus(ref Guid cmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr p return nextCommandTarget?.QueryStatus(ref cmdGroup, cCmds, prgCmds, pCmdText) ?? VSConstants.S_OK; } - - public int GetWordExtent(int iLine, int iIdx, uint dwFlags, TextSpan[] pSpan) - { - if (pSpan == null || pSpan.Length == 0) - { - return nextTextViewFilter?.GetWordExtent(iLine, iIdx, dwFlags, pSpan) ?? VSConstants.E_FAIL; - } - - bool isDoubleClick = (WORDEXTFLAGS)dwFlags == WORDEXTFLAGS.WORDEXT_FINDWORD; - - if (isDoubleClick && textView.GetBuffer(out IVsTextLines textLines) == VSConstants.S_OK) - { - textLines.GetLengthOfLine(iLine, out int iLength); - textLines.GetLineText(iLine, 0, iLine, iLength, out string lineText); - - if (TryGetQuotedStringSpan(lineText, iIdx, out int startIndex, out int endIndex)) - { - pSpan[0].iStartLine = iLine; - pSpan[0].iStartIndex = startIndex; - pSpan[0].iEndLine = iLine; - pSpan[0].iEndIndex = endIndex + 1; // TextSpan end index is exclusive - - return VSConstants.S_OK; - } - } - - return nextTextViewFilter?.GetWordExtent(iLine, iIdx, dwFlags, pSpan) ?? VSConstants.E_FAIL; - } - - public int GetPairExtents(int iLine, int iIndex, TextSpan[] pts) - { - if (nextTextViewFilter != null) - { - return nextTextViewFilter.GetPairExtents(iLine, iIndex, pts); - } - - return VSConstants.E_NOTIMPL; - } - - public int GetDataTipText(TextSpan[] pSpan, out string pbstrText) - { - pbstrText = null; - - if (nextTextViewFilter != null) - { - return nextTextViewFilter.GetDataTipText(pSpan, out pbstrText); - } - - return VSConstants.E_NOTIMPL; - } - - private static bool TryGetQuotedStringSpan(string lineText, int position, out int startIndex, out int endIndex) - { - startIndex = -1; - endIndex = -1; - - if (string.IsNullOrEmpty(lineText) || position < 0 || position > lineText.Length) - { - return false; - } - - int searchIndex = 0; - - while (searchIndex < lineText.Length) - { - int openingQuote = lineText.IndexOf('\'', searchIndex); - - if (openingQuote == -1) - { - break; - } - - int closingQuote = FindClosingQuote(lineText, openingQuote + 1); - - if (closingQuote == -1) - { - break; - } - - if (position >= openingQuote && position <= closingQuote) - { - startIndex = openingQuote; - endIndex = closingQuote; - - if (openingQuote > 0 && (lineText[openingQuote - 1] == 'N' || lineText[openingQuote - 1] == 'n')) - { - startIndex--; - } - - return true; - } - - searchIndex = closingQuote + 1; - } - - return false; - } - - private static int FindClosingQuote(string lineText, int startIndex) - { - int index = startIndex; - - while (index < lineText.Length) - { - int quoteIndex = lineText.IndexOf('\'', index); - - if (quoteIndex == -1) - { - return -1; - } - - if (quoteIndex + 1 < lineText.Length && lineText[quoteIndex + 1] == '\'') - { - index = quoteIndex + 2; // Skip escaped quotes - continue; - } - - return quoteIndex; - } - - return -1; - } } } diff --git a/AxialSqlTools/Modules/SqlStringSelectionMouseProcessor.cs b/AxialSqlTools/Modules/SqlStringSelectionMouseProcessor.cs new file mode 100644 index 0000000..852332d --- /dev/null +++ b/AxialSqlTools/Modules/SqlStringSelectionMouseProcessor.cs @@ -0,0 +1,143 @@ +using System; +using System.ComponentModel.Composition; +using System.Windows; +using System.Windows.Input; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Formatting; +using Microsoft.VisualStudio.Utilities; + +namespace AxialSqlTools +{ + [Export(typeof(IMouseProcessorProvider))] + [Name("sql-string-double-click-selector")] + [ContentType("text")] + [TextViewRole(PredefinedTextViewRoles.Document)] + public class SqlStringSelectionMouseProcessorProvider : IMouseProcessorProvider + { + public IMouseProcessor GetAssociatedProcessor(IWpfTextView wpfTextView) + { + return new SqlStringSelectionMouseProcessor(wpfTextView); + } + } + + public class SqlStringSelectionMouseProcessor : MouseProcessorBase + { + private readonly IWpfTextView view; + + public SqlStringSelectionMouseProcessor(IWpfTextView view) + { + this.view = view ?? throw new ArgumentNullException(nameof(view)); + } + + public override void PreprocessMouseLeftButtonDown(MouseButtonEventArgs e) + { + if (e.ChangedButton != MouseButton.Left) + { + return; + } + + if (e.ClickCount != 2) + { + return; + } + + if (Keyboard.Modifiers != ModifierKeys.None) + { + return; + } + + if (SelectQuotedString(e)) + { + e.Handled = true; + } + } + + private bool SelectQuotedString(MouseButtonEventArgs e) + { + SnapshotPoint? snapshotPoint = GetSnapshotAtCursor(e); + + if (snapshotPoint.HasValue && TryGetQuotedStringSpan(snapshotPoint.Value, out SnapshotSpan span)) + { + view.Selection.Select(span, isReversed: false); + view.Caret.MoveTo(span.End); + return true; + } + + return false; + } + + private SnapshotPoint? GetSnapshotAtCursor(MouseButtonEventArgs e) + { + Point cursorPosition = GetPositionInViewport(e); + ITextViewLine textViewLine = view.TextViewLines.GetTextViewLineContainingYCoordinate(cursorPosition.Y); + + if (textViewLine != null) + { + return textViewLine.GetBufferPositionFromXCoordinate(cursorPosition.X, true); + } + + return null; + } + + private Point GetPositionInViewport(MouseButtonEventArgs e) + { + Point relativePosition = e.GetPosition(view.VisualElement); + return new Point(relativePosition.X + view.ViewportLeft, relativePosition.Y + view.ViewportTop); + } + + private bool TryGetQuotedStringSpan(SnapshotPoint point, out SnapshotSpan span) + { + string text = point.Snapshot.GetText(); + int position = point.Position; + + bool insideString = false; + int stringStart = -1; + + for (int index = 0; index < text.Length; index++) + { + char current = text[index]; + + if (current == '\'') + { + if (insideString) + { + if (index + 1 < text.Length && text[index + 1] == '\'') + { + index++; + continue; + } + + int stringEnd = index; + if (position >= stringStart && position <= stringEnd) + { + span = new SnapshotSpan(point.Snapshot, stringStart, stringEnd - stringStart + 1); + return true; + } + + insideString = false; + } + else + { + stringStart = index; + if (index > 0 && (text[index - 1] == 'N' || text[index - 1] == 'n')) + { + stringStart--; + } + + insideString = true; + } + } + } + + if (insideString && position >= stringStart) + { + span = new SnapshotSpan(point.Snapshot, stringStart, text.Length - stringStart); + return true; + } + + span = default; + return false; + } + } +} From 3cf430866190d9547d1d1b837af88f4dc842e7ea Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Fri, 28 Nov 2025 13:24:49 -0700 Subject: [PATCH 6/6] Ensure mouse processor is loaded for string selection --- .../Modules/SqlStringSelectionMouseProcessor.cs | 9 ++++++++- AxialSqlTools/source.extension.vsixmanifest | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/AxialSqlTools/Modules/SqlStringSelectionMouseProcessor.cs b/AxialSqlTools/Modules/SqlStringSelectionMouseProcessor.cs index 852332d..ece85a1 100644 --- a/AxialSqlTools/Modules/SqlStringSelectionMouseProcessor.cs +++ b/AxialSqlTools/Modules/SqlStringSelectionMouseProcessor.cs @@ -12,7 +12,9 @@ namespace AxialSqlTools [Export(typeof(IMouseProcessorProvider))] [Name("sql-string-double-click-selector")] [ContentType("text")] - [TextViewRole(PredefinedTextViewRoles.Document)] + [ContentType("code")] + [Order(Before = PredefinedMouseProcessorNames.WordSelection)] + [TextViewRole(PredefinedTextViewRoles.Editable)] public class SqlStringSelectionMouseProcessorProvider : IMouseProcessorProvider { public IMouseProcessor GetAssociatedProcessor(IWpfTextView wpfTextView) @@ -69,6 +71,11 @@ private bool SelectQuotedString(MouseButtonEventArgs e) private SnapshotPoint? GetSnapshotAtCursor(MouseButtonEventArgs e) { + if (view.TextViewLines == null || view.TextViewLines.IsEmpty) + { + return null; + } + Point cursorPosition = GetPositionInViewport(e); ITextViewLine textViewLine = view.TextViewLines.GetTextViewLineContainingYCoordinate(cursorPosition.Y); diff --git a/AxialSqlTools/source.extension.vsixmanifest b/AxialSqlTools/source.extension.vsixmanifest index d4f2490..668004d 100644 --- a/AxialSqlTools/source.extension.vsixmanifest +++ b/AxialSqlTools/source.extension.vsixmanifest @@ -19,5 +19,6 @@ +