1+ // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+ namespace Microsoft.VisualStudio.FSharp.Editor
4+
5+ open System
6+ open System.Collections .Generic
7+ open System.Collections .Immutable
8+ open System.Threading
9+ open System.Threading .Tasks
10+ open System.Runtime .CompilerServices
11+
12+ open Microsoft.CodeAnalysis
13+ open Microsoft.CodeAnalysis .Completion
14+ open Microsoft.CodeAnalysis .Options
15+ open Microsoft.CodeAnalysis .Text
16+ open Microsoft.CodeAnalysis .Editor .Shared .Utilities
17+ open Microsoft.CodeAnalysis .Shared .Extensions
18+ open Microsoft.CodeAnalysis .Host
19+
20+ open Microsoft.VisualStudio .FSharp .LanguageService
21+ open Microsoft.VisualStudio .Shell
22+ open Microsoft.VisualStudio .Shell .Interop
23+ open Microsoft.VisualStudio .Text .BraceCompletion
24+
25+ open Microsoft.FSharp .Compiler
26+ open Microsoft.FSharp .Compiler .Range
27+ open Microsoft.FSharp .Compiler .SourceCodeServices
28+ open Microsoft.VisualStudio .Text .Operations
29+ open Microsoft.VisualStudio .Text
30+ open Microsoft.VisualStudio .Text .Editor
31+ open System.Diagnostics
32+ open System.Runtime .InteropServices
33+
34+ [<AutoOpen>]
35+ module private FSharpBraceCompletionSessionProviderHelpers =
36+ open System.Runtime .InteropServices
37+
38+ let getLanguageService < 'T when 'T :> ILanguageService and 'T : null > ( document : Document ) =
39+ match document.Projectwith
40+ | null -> null
41+ | project->
42+ match project.LanguageServiceswith
43+ | null -> null
44+ | languageServices->
45+ languageServices.GetService< 'T>()
46+
47+ let getCaretPoint ( buffer : ITextBuffer ) ( session : IBraceCompletionSession ) =
48+ session.TextView.Caret.Position.Point.GetPoint( buffer, PositionAffinity.Predecessor)
49+
50+ let getCaretPosition session =
51+ session|> getCaretPoint session.SubjectBuffer
52+
53+ [<Literal>]
54+ let BraceCompletion = " Brace_Completion"
55+
56+ [<AllowNullLiteral>]
57+ type internal IEditorBraceCompletionSession =
58+ inherit ILanguageService
59+
60+ abstract CheckOpeningPoint : IBraceCompletionSession * CancellationToken -> bool
61+
62+ abstract AfterStart : IBraceCompletionSession * CancellationToken -> unit
63+
64+ abstract AllowOverType : IBraceCompletionSession * CancellationToken -> bool
65+
66+ abstract AfterReturn : IBraceCompletionSession * CancellationToken -> unit
67+
68+ [<AllowNullLiteral>]
69+ type internal IEditorBraceCompletionSessionFactory =
70+ inherit ILanguageService
71+
72+ abstract TryCreateSession : Document * openingPosition : int * openingBrace : char * CancellationToken -> IEditorBraceCompletionSession
73+
74+ type internal BraceCompletionSession
75+ (
76+ textView: ITextView,
77+ subjectBuffer: ITextBuffer,
78+ openingPoint: SnapshotPoint,
79+ openingBrace: char,
80+ closingBrace: char,
81+ undoHistory: ITextUndoHistory,
82+ editorOperationsFactoryService: IEditorOperationsFactoryService,
83+ session: IEditorBraceCompletionSession
84+ ) =
85+
86+ let mutable closingPoint = subjectBuffer.CurrentSnapshot.CreateTrackingPoint( openingPoint.Position, PointTrackingMode.Positive)
87+ let mutable openingPoint : ITrackingPoint = null
88+ let editorOperations = editorOperationsFactoryService.GetEditorOperations( textView)
89+
90+ member this.EndSession () =
91+ closingPoint<- null
92+ openingPoint<- null
93+
94+ member __.CreateUndoTransaction () =
95+ undoHistory.CreateTransaction( BraceCompletion)
96+
97+ member this.Start ( cancellationToken : CancellationToken ) =
98+ // this is where the caret should go after the change
99+ let pos = textView.Caret.Position.BufferPosition
100+ let beforeTrackingPoint = pos.Snapshot.CreateTrackingPoint( pos.Position, PointTrackingMode.Negative)
101+
102+ let mutable snapshot = subjectBuffer.CurrentSnapshot
103+ let closingSnapshotPoint = closingPoint.GetPoint( snapshot)
104+
105+ if closingSnapshotPoint.Position< 1 then
106+ Debug.Fail( " The closing point was not found at the expected position." )
107+ this.EndSession()
108+ else
109+
110+ let openingSnapshotPoint = closingSnapshotPoint.Subtract( 1 )
111+
112+ if openingSnapshotPoint.GetChar() <> openingBracethen
113+ // there is a bug in editor brace completion engine on projection buffer that already fixed in vs_pro. until that is FIed to use
114+ // I will make this not to assert
115+ // Debug.Fail("The opening brace was not found at the expected position.");
116+ this.EndSession()
117+ else
118+
119+ openingPoint<- snapshot.CreateTrackingPoint( openingSnapshotPoint.Position, PointTrackingMode.Positive)
120+ let _document = snapshot.GetOpenDocumentInCurrentContextWithChanges()
121+
122+ if not ( session.CheckOpeningPoint( this, cancellationToken)) then
123+ this.EndSession()
124+ else
125+
126+ use undo= this.CreateUndoTransaction()
127+
128+ snapshot<-
129+ use edit= subjectBuffer.CreateEdit()
130+
131+ if edit.HasFailedChangesthen
132+ Debug.Fail( " Unable to insert closing brace" )
133+
134+ // exit without setting the closing point which will take us off the stack
135+ edit.Cancel()
136+ undo.Cancel()
137+ snapshot
138+ else
139+ edit.Apply() // FIXME: perhaps, it should be ApplyAndLogExceptions()
140+
141+ let beforePoint = beforeTrackingPoint.GetPoint( textView.TextSnapshot)
142+
143+ // switch from positive to negative tracking so it stays against the closing brace
144+ closingPoint<- subjectBuffer.CurrentSnapshot.CreateTrackingPoint( closingPoint.GetPoint( snapshot) .Position, PointTrackingMode.Negative)
145+
146+ Debug.Assert( closingPoint.GetPoint( snapshot) .Position> 0 && ( SnapshotSpan( closingPoint.GetPoint( snapshot) .Subtract( 1 ), 1 )) .GetText() .Equals( closingBrace.ToString()),
147+ " The closing point does not match the closing brace character" )
148+
149+ // move the caret back between the braces
150+ textView.Caret.MoveTo( beforePoint) |> ignore
151+
152+ session.AfterStart( this, cancellationToken)
153+
154+ undo.Complete()
155+
156+ member this.HasNoForwardTyping ( caretPoint : SnapshotPoint , endPoint : SnapshotPoint ) =
157+ Debug.Assert( caretPoint.Snapshot= endPoint.Snapshot, " snapshots do not match" )
158+
159+ if caretPoint.Snapshot= endPoint.Snapshotthen
160+ if caretPoint= endPointthen
161+ true
162+ else
163+ if caretPoint.Position< endPoint.Positionthen
164+ let span = SnapshotSpan( caretPoint, endPoint)
165+ String.IsNullOrWhiteSpace( span.GetText())
166+ else
167+ false
168+ else
169+ false
170+
171+ member this.HasForwardTyping =
172+ let closingSnapshotPoint = closingPoint.GetPoint( subjectBuffer.CurrentSnapshot)
173+
174+ if closingSnapshotPoint.Position> 0 then
175+ let caretPos = getCaretPosition this
176+
177+ if caretPos.HasValue&& not ( this.HasNoForwardTyping( caretPos.Value, closingSnapshotPoint.Subtract( 1 ))) then
178+ true
179+ else
180+ false
181+ else
182+ false
183+
184+ member __.MoveCaretToClosingPoint () =
185+ let closingSnapshotPoint = closingPoint.GetPoint( subjectBuffer.CurrentSnapshot)
186+
187+ // find the position just after the closing brace in the view's text buffer
188+ let afterBrace =
189+ textView.BufferGraph.MapUpToBuffer( closingSnapshotPoint, PointTrackingMode.Negative, PositionAffinity.Predecessor, textView.TextBuffer)
190+
191+ Debug.Assert( afterBrace.HasValue, " Unable to move caret to closing point" )
192+
193+ if afterBrace.HasValuethen
194+ textView.Caret.MoveTo( afterBrace.Value) |> ignore
195+
196+
197+ interface IBraceCompletionSessionwith
198+
199+ member this.Start () =
200+ // Brace completion is not cancellable.
201+ this.Start( CancellationToken.None)
202+
203+ member this.PreBackspace handledCommand =
204+ handledCommand<- false
205+
206+ let caretPos = getCaretPosition this
207+ let snapshot = subjectBuffer.CurrentSnapshot
208+
209+ if caretPos.HasValue&& caretPos.Value.Position> 0 &&
210+ caretPos.Value.Position- 1 = openingPoint.GetPoint( snapshot) .Position&&
211+ not this.HasForwardTypingthen
212+
213+ use undo= this.CreateUndoTransaction()
214+ use edit= subjectBuffer.CreateEdit()
215+
216+ let span = SnapshotSpan( openingPoint.GetPoint( snapshot), closingPoint.GetPoint( snapshot))
217+
218+ edit.Delete( span.Span) |> ignore
219+
220+ if edit.HasFailedChangesthen
221+ edit.Cancel()
222+ undo.Cancel()
223+ Debug.Fail( " Unable to clear braces" )
224+ else
225+ // handle the command so the backspace does
226+ // not go through since we've already cleared the braces
227+ handledCommand<- true
228+ edit.Apply() |> ignore// FIXME: ApplyAndLogExceptions()
229+ undo.Complete()
230+ this.EndSession()
231+
232+ member __.PostBackspace () = ()
233+
234+ member this.PreOverType handledCommand =
235+ handledCommand<- false
236+
237+ if closingPoint<> null then
238+ // Brace completion is not cancellable.
239+ let cancellationToken = CancellationToken.None
240+ let snapshot = subjectBuffer.CurrentSnapshot
241+ let _document = snapshot.GetOpenDocumentInCurrentContextWithChanges()
242+
243+ let closingSnapshotPoint = closingPoint.GetPoint( snapshot)
244+ if not this.HasForwardTyping&& session.AllowOverType( this, cancellationToken) then
245+ let caretPos = getCaretPosition this
246+
247+ Debug.Assert( caretPos.HasValue&& caretPos.Value.Position< closingSnapshotPoint.Position)
248+
249+ // ensure that we are within the session before clearing
250+ if caretPos.HasValue&& caretPos.Value.Position< closingSnapshotPoint.Position&& closingSnapshotPoint.Position> 0 then
251+
252+ use undo= this.CreateUndoTransaction()
253+
254+ let span = SnapshotSpan( caretPos.Value, closingSnapshotPoint.Subtract( 1 ))
255+
256+ use edit= subjectBuffer.CreateEdit()
257+
258+ edit.Delete( span.Span) |> ignore
259+
260+ if edit.HasFailedChangesthen
261+ Debug.Fail( " Unable to clear closing brace" )
262+ edit.Cancel()
263+ undo.Cancel()
264+ else
265+ handledCommand<- true
266+ edit.Apply() |> ignore// FIXME: ApplyAndLogExceptions()
267+ this.MoveCaretToClosingPoint()
268+ editorOperations.AddAfterTextBufferChangePrimitive()
269+ undo.Complete()
270+
271+ member __.PostOverType () = ()
272+
273+ member this.PreTab handledCommand =
274+ handledCommand<- false
275+
276+ if not this.HasForwardTypingthen
277+ handledCommand<- true
278+
279+ use undo= this.CreateUndoTransaction()
280+
281+ editorOperations.AddBeforeTextBufferChangePrimitive()
282+ this.MoveCaretToClosingPoint()
283+ editorOperations.AddAfterTextBufferChangePrimitive()
284+ undo.Complete()
285+
286+ member __.PreReturn handledCommand =
287+ handledCommand<- false
288+
289+ member this.PostReturn () =
290+ let caretPos = getCaretPosition this
291+
292+ if caretPos.HasValuethen
293+ let closingSnapshotPoint = closingPoint.GetPoint( subjectBuffer.CurrentSnapshot)
294+
295+ if closingSnapshotPoint.Position> 0 && this.HasNoForwardTyping( caretPos.Value, closingSnapshotPoint.Subtract( 1 )) then
296+ session.AfterReturn( this, CancellationToken.None)
297+
298+ member __.Finish () = ()
299+
300+ member __.PostTab () = ()
301+
302+ member __.PreDelete handledCommand =
303+ handledCommand<- false
304+
305+ member __.PostDelete () = ()
306+
307+ member __.OpeningBrace = openingBrace
308+
309+ member __.ClosingBrace = closingBrace
310+
311+ member __.OpeningPoint = openingPoint
312+
313+ member __.ClosingPoint = closingPoint
314+
315+ member __.SubjectBuffer = subjectBuffer
316+
317+ member __.TextView = textView
318+
319+ type internal BraceCompletionSessionProvider
320+ (
321+ undoManager: ITextBufferUndoManagerProvider,
322+ _ editorOperationsFactoryService: IEditorOperationsFactoryService
323+ ) =
324+
325+ inherit ForegroundThreadAffinitizedObject()
326+
327+ interface IBraceCompletionSessionProviderwith
328+
329+ member this.TryCreateSession ( textView , openingPoint , openingBrace , _closingBrace , session ) =
330+ this.AssertIsForeground();
331+
332+ let textSnapshot = openingPoint.Snapshot
333+
334+ session<-
335+ match textSnapshot.GetOpenDocumentInCurrentContextWithChanges() with
336+ | null -> null
337+ | document->
338+ match getLanguageService< IEditorBraceCompletionSessionFactory> documentwith
339+ | null -> null
340+ | editorSessionFactory->
341+ // Brace completion is (currently) not cancellable.
342+ let cancellationToken = CancellationToken.None
343+
344+ match editorSessionFactory.TryCreateSession( document, openingPoint.Position, openingBrace, cancellationToken) with
345+ | null -> null
346+ | _ editorSession->
347+ let _undoHistory = undoManager.GetTextBufferUndoManager( textView.TextBuffer) .TextBufferUndoHistory
348+ null
349+
350+ match sessionwith
351+ | null -> false
352+ | _ -> true