1
1
using System ;
2
2
using System . Collections . Generic ;
3
+ using System . Diagnostics ;
4
+ using System . IO ;
3
5
using System . Linq ;
6
+ using System . Runtime . InteropServices ;
7
+ using System . Text ;
8
+ using System . Threading ;
4
9
using System . Threading . Tasks ;
10
+ using System . Web ;
5
11
using EditorServicesCommandSuite . Internal ;
6
12
using Microsoft . PowerShell . EditorServices ;
7
13
using Microsoft . PowerShell . EditorServices . Protocol . LanguageServer ;
@@ -10,6 +16,8 @@ namespace EditorServicesCommandSuite.EditorServices
10
16
{
11
17
internal class DocumentService : IDocumentEditProcessor
12
18
{
19
+ private const string FileUriPrefix = "file:///" ;
20
+
13
21
private readonly EditorSession _editorSession ;
14
22
15
23
private readonly MessageService _messages ;
@@ -20,33 +28,51 @@ internal DocumentService(EditorSession editorSession, MessageService messages)
20
28
_messages = messages ;
21
29
}
22
30
23
- public async Task WriteDocumentEditsAsync ( IEnumerable < DocumentEdit > edits )
31
+ public async Task WriteDocumentEditsAsync ( IEnumerable < DocumentEdit > edits , CancellationToken cancellationToken )
24
32
{
25
- var context = await _messages . SendRequestAsync (
26
- GetEditorContextRequest . Type ,
27
- new GetEditorContextRequest ( ) ,
28
- waitForResponse : true ) ;
33
+ ClientEditorContext context = await GetClientContext ( ) ;
34
+
35
+ // Order by empty file names first so the first group processed is the current file.
36
+ IOrderedEnumerable < IGrouping < string , DocumentEdit > > orderedGroups = edits
37
+ . GroupBy ( e => e . FileName )
38
+ . OrderByDescending ( g => string . IsNullOrEmpty ( g . Key ) ) ;
29
39
30
- var scriptFile = _editorSession . Workspace . GetFile ( context . CurrentFilePath ) ;
31
- foreach ( var edit in edits . OrderByDescending ( edit => edit . StartOffset ) )
40
+ foreach ( IGrouping < string , DocumentEdit > editGroup in orderedGroups )
32
41
{
33
- var request = new InsertTextRequest ( )
42
+ ScriptFile scriptFile ;
43
+ try
34
44
{
35
- FilePath = scriptFile . ClientFilePath ,
36
- InsertText = edit . NewValue ,
37
- InsertRange = new Range ( )
45
+ scriptFile = _editorSession . Workspace . GetFile (
46
+ string . IsNullOrEmpty ( editGroup . Key ) ? context . CurrentFilePath : editGroup . Key ) ;
47
+ }
48
+ catch ( FileNotFoundException )
49
+ {
50
+ scriptFile = await CreateNewFile ( context , editGroup . Key , cancellationToken ) ;
51
+ }
52
+
53
+ // ScriptFile.ClientFilePath isn't always a URI.
54
+ string clientFilePath = GetPathAsClientPath ( scriptFile . ClientFilePath ) ;
55
+ foreach ( var edit in editGroup . OrderByDescending ( edit => edit . StartOffset ) )
56
+ {
57
+ cancellationToken . ThrowIfCancellationRequested ( ) ;
58
+ var request = new InsertTextRequest ( )
38
59
{
39
- Start = ToServerPosition ( scriptFile . GetPositionAtOffset ( ( int ) edit . StartOffset ) ) ,
40
- End = ToServerPosition ( scriptFile . GetPositionAtOffset ( ( int ) edit . EndOffset ) ) ,
41
- } ,
42
- } ;
60
+ FilePath = clientFilePath ,
61
+ InsertText = edit . NewValue ,
62
+ InsertRange = new Range ( )
63
+ {
64
+ Start = ToServerPosition ( scriptFile . GetPositionAtOffset ( ( int ) edit . StartOffset ) ) ,
65
+ End = ToServerPosition ( scriptFile . GetPositionAtOffset ( ( int ) edit . EndOffset ) ) ,
66
+ } ,
67
+ } ;
43
68
44
- await _messages . SendRequestAsync (
45
- InsertTextRequest . Type ,
46
- request ,
47
- waitForResponse : true ) ;
69
+ await _messages . SendRequestAsync (
70
+ InsertTextRequest . Type ,
71
+ request ,
72
+ waitForResponse : true ) ;
48
73
49
- await Task . Delay ( TimeSpan . FromMilliseconds ( 50 ) ) ;
74
+ await Task . Delay ( TimeSpan . FromMilliseconds ( 50 ) , cancellationToken ) ;
75
+ }
50
76
}
51
77
}
52
78
@@ -58,5 +84,88 @@ internal static Position ToServerPosition(BufferPosition position)
58
84
Character = position . Column - 1 ,
59
85
} ;
60
86
}
87
+
88
+ private static string GetPathAsClientPath ( string path )
89
+ {
90
+ Debug . Assert (
91
+ ! string . IsNullOrWhiteSpace ( path ) ,
92
+ "Caller should verify path is valid" ) ;
93
+
94
+ if ( path . StartsWith ( "file:///" , StringComparison . Ordinal ) )
95
+ {
96
+ return path ;
97
+ }
98
+
99
+ Debug . Assert (
100
+ Path . IsPathRooted ( path ) ,
101
+ "EditorServices saved an unrooted, non-URI path to ClientFilePath" ) ;
102
+
103
+ if ( ! RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
104
+ {
105
+ return new Uri ( path ) . AbsoluteUri ;
106
+ }
107
+
108
+ // VSCode file URIs on Windows need the drive letter lowercase, and the colon
109
+ // URI encoded. System.Uri won't do that, so we manually create the URI.
110
+ var newUri = new StringBuilder ( HttpUtility . UrlPathEncode ( path ) ) ;
111
+ int colonIndex = path . IndexOf ( Symbols . Colon ) ;
112
+ for ( var i = colonIndex - 1 ; i >= 0 ; i -- )
113
+ {
114
+ newUri . Remove ( i , 1 ) ;
115
+ newUri . Insert ( i , char . ToLowerInvariant ( path [ i ] ) ) ;
116
+ }
117
+
118
+ return newUri
119
+ . Remove ( colonIndex , 1 )
120
+ . Insert ( colonIndex , "%3A" )
121
+ . Replace ( Symbols . Backslash , Symbols . ForwardSlash )
122
+ . Insert ( 0 , FileUriPrefix )
123
+ . ToString ( ) ;
124
+ }
125
+
126
+ private async Task < ScriptFile > CreateNewFile (
127
+ ClientEditorContext context ,
128
+ string path ,
129
+ CancellationToken cancellationToken )
130
+ {
131
+ // Path parameter doesn't actually do anything currently. The new file will be untitled.
132
+ await _messages . SendRequestAsync (
133
+ NewFileRequest . Type ,
134
+ path ,
135
+ true ) ;
136
+
137
+ ClientEditorContext newContext ;
138
+ while ( true )
139
+ {
140
+ newContext = await GetClientContext ( ) ;
141
+ if ( ! newContext . CurrentFilePath . Equals ( context . CurrentFilePath , StringComparison . OrdinalIgnoreCase ) )
142
+ {
143
+ break ;
144
+ }
145
+
146
+ await Task . Delay ( 200 , cancellationToken ) ;
147
+ }
148
+
149
+ ScriptFile scriptFile = _editorSession . Workspace . GetFile ( newContext . CurrentFilePath ) ;
150
+ await _messages . SendRequestAsync (
151
+ SaveFileRequest . Type ,
152
+ new SaveFileDetails ( )
153
+ {
154
+ FilePath = scriptFile . ClientFilePath ,
155
+ NewPath = path ,
156
+ } ,
157
+ waitForResponse : true ) ;
158
+
159
+ cancellationToken . ThrowIfCancellationRequested ( ) ;
160
+ return _editorSession . Workspace . GetFile ( path ) ;
161
+ }
162
+
163
+ private async Task < ClientEditorContext > GetClientContext ( )
164
+ {
165
+ return await _messages . SendRequestAsync (
166
+ GetEditorContextRequest . Type ,
167
+ new GetEditorContextRequest ( ) ,
168
+ waitForResponse : true ) ;
169
+ }
61
170
}
62
171
}
0 commit comments