Skip to content

Commit 4aa9392

Browse files
authored
feat(documents): implement custom document viewer editor (#329)
1 parent ceb1136 commit 4aa9392

10 files changed

+461
-2
lines changed

src/CodingWithCalvin.CouchbaseExplorer/CodingWithCalvin.CouchbaseExplorer.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
<Compile Include="Services\ConnectionSettingsService.cs" />
8080
<Compile Include="Services\CouchbaseService.cs" />
8181
<Compile Include="Services\CredentialManagerService.cs" />
82+
<Compile Include="Services\DocumentEditorService.cs" />
8283
<Compile Include="ViewModels\BindingProxy.cs" />
8384
<Compile Include="ViewModels\BucketNode.cs" />
8485
<Compile Include="ViewModels\BucketsFolderNode.cs" />
@@ -99,12 +100,21 @@
99100
<Compile Include="Dialogs\ConnectionDialog.xaml.cs">
100101
<DependentUpon>ConnectionDialog.xaml</DependentUpon>
101102
</Compile>
103+
<Compile Include="Editors\DocumentEditorControl.xaml.cs">
104+
<DependentUpon>DocumentEditorControl.xaml</DependentUpon>
105+
</Compile>
106+
<Compile Include="Editors\DocumentEditorFactory.cs" />
107+
<Compile Include="Editors\DocumentEditorPane.cs" />
102108
</ItemGroup>
103109
<ItemGroup>
104110
<Page Include="Dialogs\ConnectionDialog.xaml">
105111
<SubType>Designer</SubType>
106112
<Generator>MSBuild:Compile</Generator>
107113
</Page>
114+
<Page Include="Editors\DocumentEditorControl.xaml">
115+
<SubType>Designer</SubType>
116+
<Generator>MSBuild:Compile</Generator>
117+
</Page>
108118
</ItemGroup>
109119
<ItemGroup>
110120
<Resource Include="Resources\watermark.png" />

src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerPackage.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Runtime.InteropServices;
33
using System.Threading;
4+
using CodingWithCalvin.CouchbaseExplorer.Editors;
45
using Microsoft.VisualStudio.Shell;
56
using Microsoft.VisualStudio.Shell.Interop;
67

@@ -16,18 +17,35 @@ namespace CodingWithCalvin.CouchbaseExplorer
1617
Window = ToolWindowGuids.ServerExplorer)]
1718
[ProvideAutoLoad(UIContextGuids80.NoSolution, PackageAutoLoadFlags.BackgroundLoad)]
1819
[ProvideAutoLoad(UIContextGuids80.SolutionExists, PackageAutoLoadFlags.BackgroundLoad)]
20+
[ProvideEditorFactory(typeof(DocumentEditorFactory), 110, TrustLevel = __VSEDITORTRUSTLEVEL.ETL_AlwaysTrusted)]
21+
[ProvideEditorExtension(typeof(DocumentEditorFactory), ".cbjson", 50)]
1922
public sealed class CouchbaseExplorerPackage : AsyncPackage
2023
{
2124
public const string PackageGuidString = "ef261503-b2ae-4b90-8c86-0becd83348cc";
2225

26+
private DocumentEditorFactory _editorFactory;
27+
2328
protected override async System.Threading.Tasks.Task InitializeAsync(
2429
CancellationToken cancellationToken,
2530
IProgress<ServiceProgressData> progress
2631
)
2732
{
2833
await JoinableTaskFactory.SwitchToMainThreadAsync();
2934

35+
// Register the editor factory
36+
_editorFactory = new DocumentEditorFactory();
37+
RegisterEditorFactory(_editorFactory);
38+
3039
CouchbaseExplorerWindowCommand.Initialize(this);
3140
}
41+
42+
protected override void Dispose(bool disposing)
43+
{
44+
if (disposing)
45+
{
46+
_editorFactory?.Dispose();
47+
}
48+
base.Dispose(disposing);
49+
}
3250
}
3351
}

src/CodingWithCalvin.CouchbaseExplorer/CouchbaseExplorerWindowControl.xaml.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public CouchbaseExplorerWindowControl()
1919

2020
ExplorerTreeView.SelectedItemChanged += OnSelectedItemChanged;
2121
ExplorerTreeView.PreviewMouseRightButtonDown += OnPreviewMouseRightButtonDown;
22+
ExplorerTreeView.MouseDoubleClick += OnMouseDoubleClick;
2223
}
2324

2425
private void OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
@@ -40,6 +41,15 @@ private void OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e
4041
}
4142
}
4243

44+
private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
45+
{
46+
if (ViewModel.SelectedNode is DocumentNode)
47+
{
48+
ViewModel.OpenDocumentCommand.Execute(null);
49+
e.Handled = true;
50+
}
51+
}
52+
4353
private static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
4454
{
4555
while (current != null)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<UserControl x:Class="CodingWithCalvin.CouchbaseExplorer.Editors.DocumentEditorControl"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:vsshell="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
5+
Background="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowBackgroundKey}}"
6+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowTextKey}}">
7+
8+
<Grid>
9+
<Grid.RowDefinitions>
10+
<RowDefinition Height="Auto" />
11+
<RowDefinition Height="*" />
12+
</Grid.RowDefinitions>
13+
14+
<!-- Header with document info and actions -->
15+
<Border Grid.Row="0"
16+
Background="{DynamicResource {x:Static vsshell:VsBrushes.CommandBarGradientBeginKey}}"
17+
BorderBrush="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowBorderKey}}"
18+
BorderThickness="0,0,0,1"
19+
Padding="10,8">
20+
<Grid>
21+
<Grid.ColumnDefinitions>
22+
<ColumnDefinition Width="*" />
23+
<ColumnDefinition Width="Auto" />
24+
</Grid.ColumnDefinitions>
25+
26+
<!-- Document Info -->
27+
<StackPanel Grid.Column="0">
28+
<TextBlock x:Name="DocumentIdText" FontWeight="Bold" FontSize="14"
29+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowTextKey}}" />
30+
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
31+
<TextBlock Text="Collection: " FontSize="11" Opacity="0.7"
32+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowTextKey}}" />
33+
<TextBlock x:Name="CollectionPathText" FontSize="11" Opacity="0.7"
34+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowTextKey}}" />
35+
</StackPanel>
36+
<StackPanel Orientation="Horizontal" Margin="0,2,0,0">
37+
<TextBlock Text="CAS: " FontSize="11" Opacity="0.7"
38+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowTextKey}}" />
39+
<TextBlock x:Name="CasText" FontSize="11" Opacity="0.7"
40+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowTextKey}}" />
41+
</StackPanel>
42+
</StackPanel>
43+
44+
<!-- Action Buttons -->
45+
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
46+
<Button x:Name="RefreshButton" Content="Refresh" Padding="12,4" Margin="0,0,8,0"
47+
Click="RefreshButton_Click"
48+
Background="{DynamicResource {x:Static vsshell:VsBrushes.CommandBarGradientBeginKey}}"
49+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.CommandBarTextActiveKey}}" />
50+
<Button x:Name="CopyButton" Content="Copy JSON" Padding="12,4"
51+
Click="CopyButton_Click"
52+
Background="{DynamicResource {x:Static vsshell:VsBrushes.CommandBarGradientBeginKey}}"
53+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.CommandBarTextActiveKey}}" />
54+
</StackPanel>
55+
</Grid>
56+
</Border>
57+
58+
<!-- JSON Content -->
59+
<TextBox Grid.Row="1"
60+
x:Name="JsonContentBox"
61+
IsReadOnly="True"
62+
FontFamily="Consolas"
63+
FontSize="13"
64+
AcceptsReturn="True"
65+
TextWrapping="NoWrap"
66+
VerticalScrollBarVisibility="Auto"
67+
HorizontalScrollBarVisibility="Auto"
68+
Background="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowBackgroundKey}}"
69+
Foreground="{DynamicResource {x:Static vsshell:VsBrushes.ToolWindowTextKey}}"
70+
BorderThickness="0"
71+
Padding="10" />
72+
</Grid>
73+
</UserControl>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System;
2+
using System.Windows;
3+
using System.Windows.Controls;
4+
using CodingWithCalvin.CouchbaseExplorer.Services;
5+
using Newtonsoft.Json;
6+
7+
namespace CodingWithCalvin.CouchbaseExplorer.Editors
8+
{
9+
public partial class DocumentEditorControl : UserControl
10+
{
11+
public string ConnectionId { get; private set; }
12+
public string BucketName { get; private set; }
13+
public string ScopeName { get; private set; }
14+
public string CollectionName { get; private set; }
15+
public string DocumentId { get; private set; }
16+
public ulong Cas { get; private set; }
17+
18+
public DocumentEditorControl()
19+
{
20+
InitializeComponent();
21+
}
22+
23+
public void SetDocument(string connectionId, string documentId, string bucketName, string scopeName, string collectionName, object content, ulong cas)
24+
{
25+
ConnectionId = connectionId;
26+
DocumentId = documentId;
27+
BucketName = bucketName;
28+
ScopeName = scopeName;
29+
CollectionName = collectionName;
30+
Cas = cas;
31+
32+
DocumentIdText.Text = documentId;
33+
CollectionPathText.Text = $"{bucketName}.{scopeName}.{collectionName}";
34+
CasText.Text = cas.ToString();
35+
36+
if (content != null)
37+
{
38+
JsonContentBox.Text = JsonConvert.SerializeObject(content, Formatting.Indented);
39+
}
40+
else
41+
{
42+
JsonContentBox.Text = "null";
43+
}
44+
}
45+
46+
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
47+
{
48+
try
49+
{
50+
RefreshButton.IsEnabled = false;
51+
RefreshButton.Content = "Refreshing...";
52+
53+
var content = await CouchbaseService.GetDocumentAsync(
54+
ConnectionId,
55+
BucketName,
56+
ScopeName,
57+
CollectionName,
58+
DocumentId);
59+
60+
Cas = content.Cas;
61+
CasText.Text = content.Cas.ToString();
62+
63+
if (content.Content != null)
64+
{
65+
JsonContentBox.Text = JsonConvert.SerializeObject(content.Content, Formatting.Indented);
66+
}
67+
else
68+
{
69+
JsonContentBox.Text = "null";
70+
}
71+
}
72+
catch (Exception ex)
73+
{
74+
MessageBox.Show(
75+
$"Failed to refresh document: {ex.Message}",
76+
"Error",
77+
MessageBoxButton.OK,
78+
MessageBoxImage.Error);
79+
}
80+
finally
81+
{
82+
RefreshButton.IsEnabled = true;
83+
RefreshButton.Content = "Refresh";
84+
}
85+
}
86+
87+
private void CopyButton_Click(object sender, RoutedEventArgs e)
88+
{
89+
Clipboard.SetText(JsonContentBox.Text);
90+
}
91+
}
92+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using Microsoft.VisualStudio;
4+
using Microsoft.VisualStudio.Shell;
5+
using Microsoft.VisualStudio.Shell.Interop;
6+
using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
7+
8+
namespace CodingWithCalvin.CouchbaseExplorer.Editors
9+
{
10+
[Guid("B7D4E2F1-8A3C-4E5D-9F6A-2B1C3D4E5F6A")]
11+
public class DocumentEditorFactory : IVsEditorFactory, IDisposable
12+
{
13+
private ServiceProvider _serviceProvider;
14+
15+
public int SetSite(IOleServiceProvider psp)
16+
{
17+
_serviceProvider = new ServiceProvider(psp);
18+
return VSConstants.S_OK;
19+
}
20+
21+
public int MapLogicalView(ref Guid rguidLogicalView, out string pbstrPhysicalView)
22+
{
23+
pbstrPhysicalView = null;
24+
25+
if (rguidLogicalView == VSConstants.LOGVIEWID_Primary ||
26+
rguidLogicalView == VSConstants.LOGVIEWID_TextView ||
27+
rguidLogicalView == Guid.Empty)
28+
{
29+
return VSConstants.S_OK;
30+
}
31+
32+
return VSConstants.E_NOTIMPL;
33+
}
34+
35+
public int CreateEditorInstance(
36+
uint grfCreateDoc,
37+
string pszMkDocument,
38+
string pszPhysicalView,
39+
IVsHierarchy pvHier,
40+
uint itemid,
41+
IntPtr punkDocDataExisting,
42+
out IntPtr ppunkDocView,
43+
out IntPtr ppunkDocData,
44+
out string pbstrEditorCaption,
45+
out Guid pguidCmdUI,
46+
out int pgrfCDW)
47+
{
48+
ppunkDocView = IntPtr.Zero;
49+
ppunkDocData = IntPtr.Zero;
50+
pbstrEditorCaption = string.Empty;
51+
pguidCmdUI = Guid.Empty;
52+
pgrfCDW = 0;
53+
54+
// Create a new editor pane
55+
var editorPane = new DocumentEditorPane();
56+
57+
ppunkDocView = Marshal.GetIUnknownForObject(editorPane);
58+
ppunkDocData = Marshal.GetIUnknownForObject(editorPane);
59+
pbstrEditorCaption = "";
60+
pguidCmdUI = typeof(DocumentEditorPane).GUID;
61+
62+
return VSConstants.S_OK;
63+
}
64+
65+
public int Close()
66+
{
67+
return VSConstants.S_OK;
68+
}
69+
70+
public void Dispose()
71+
{
72+
Dispose(true);
73+
GC.SuppressFinalize(this);
74+
}
75+
76+
protected virtual void Dispose(bool disposing)
77+
{
78+
if (disposing)
79+
{
80+
_serviceProvider?.Dispose();
81+
_serviceProvider = null;
82+
}
83+
}
84+
}
85+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using Microsoft.VisualStudio.Shell;
4+
5+
namespace CodingWithCalvin.CouchbaseExplorer.Editors
6+
{
7+
[Guid("E8A3C5D1-9B2F-4A6C-8D7E-1F2A3B4C5D6E")]
8+
public class DocumentEditorPane : WindowPane
9+
{
10+
private readonly DocumentEditorControl _control;
11+
12+
public DocumentEditorControl EditorControl => _control;
13+
14+
public DocumentEditorPane() : base(null)
15+
{
16+
_control = new DocumentEditorControl();
17+
Content = _control;
18+
}
19+
20+
public void SetDocument(string connectionId, string documentId, string bucketName, string scopeName, string collectionName, object content, ulong cas)
21+
{
22+
_control.SetDocument(connectionId, documentId, bucketName, scopeName, collectionName, content, cas);
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)