1313using Microsoft . Scripting ;
1414using System . Threading ;
1515using System . Reflection ;
16+ using System . Text . RegularExpressions ;
1617
1718namespace PythonConsoleControl
1819{
@@ -37,54 +38,68 @@ public PythonConsoleCompletionDataProvider(CommandLine commandLine)//IMemberProv
3738 /// the dot character that triggered the completion. The text can contain the command line prompt
3839 /// '>>>' as this will be ignored.
3940 /// </summary>
40- public ICompletionData [ ] GenerateCompletionData ( string line )
41+ public Tuple < ICompletionData [ ] , string , string > GenerateCompletionData ( string line )
4142 {
4243 List < PythonCompletionData > items = new List < PythonCompletionData > ( ) ; //DefaultCompletionData
4344
44- string name = GetName ( line ) ;
45+ string objectName = string . Empty ;
46+ string memberName = string . Empty ;
47+
48+ int lastDelimiterIndex = FindLastDelimiter ( line ) ;
49+
50+ string name = line . Substring ( lastDelimiterIndex + 1 ) ;
51+
4552 // A very simple test of callables!
46- if ( excludeCallables && name . Contains ( ')' ) ) return null ;
53+ bool isCallable = name . Contains ( ')' ) ;
54+
55+ if ( excludeCallables && isCallable ) return null ;
4756
48- if ( ! String . IsNullOrEmpty ( name ) )
57+ System . IO . Stream stream = commandLine . ScriptScope . Engine . Runtime . IO . OutputStream ;
58+ try
4959 {
50- System . IO . Stream stream = commandLine . ScriptScope . Engine . Runtime . IO . OutputStream ;
51- try
60+ AutocompletionInProgress = true ;
61+ // Another possibility:
62+ //commandLine.ScriptScope.Engine.Runtime.IO.SetOutput(new System.IO.MemoryStream(), Encoding.UTF8);
63+ //object value = commandLine.ScriptScope.Engine.CreateScriptSourceFromString(name, SourceCodeKind.Expression).Execute(commandLine.ScriptScope);
64+ //IList<string> members = commandLine.ScriptScope.Engine.Operations.GetMemberNames(value);
65+
66+ var lastWord = GetLastWord ( name ) ;
67+ var beforeLastWord = name . Substring ( 0 , name . Length - lastWord . Length ) ;
68+ if ( beforeLastWord . EndsWith ( "." ) )
5269 {
53- AutocompletionInProgress = true ;
54- // Another possibility:
55- //commandLine.ScriptScope.Engine.Runtime.IO.SetOutput(new System.IO.MemoryStream(), Encoding.UTF8);
56- //object value = commandLine.ScriptScope.Engine.CreateScriptSourceFromString(name, SourceCodeKind.Expression).Execute(commandLine.ScriptScope);
57- //IList<string> members = commandLine.ScriptScope.Engine.Operations.GetMemberNames(value);
58- Type type = TryGetType ( name ) ;
59- // Use Reflection for everything except in-built Python types and COM pbjects.
60- if ( type != null && type . Namespace != "IronPython.Runtime" && ! type . FullName . Contains ( "IronPython.NewTypes" ) && ( type . Name != "__ComObject" ) )
61- {
62- PopulateFromCLRType ( items , type , name ) ;
63- }
64- else
65- {
66- //string dirCommand = "dir(" + name + ")";
67- string dirCommand = "sorted([m for m in dir(" + name + ") if not m.startswith('__')], key = str.lower) + sorted([m for m in dir(" + name + ") if m.startswith('__')])" ;
68- object value = commandLine . ScriptScope . Engine . CreateScriptSourceFromString ( dirCommand , SourceCodeKind . Expression ) . Execute ( commandLine . ScriptScope ) ;
69- AutocompletionInProgress = false ;
70- foreach ( object member in ( value as IronPython . Runtime . List ) )
71- {
72- items . Add ( new PythonCompletionData ( ( string ) member , name , commandLine , false ) ) ;
73- }
74- }
70+ objectName = beforeLastWord . Substring ( 0 , beforeLastWord . Length - 1 ) ;
71+ memberName = lastWord ;
7572 }
76- catch ( ThreadAbortException tae )
73+ else
7774 {
78- if ( tae . ExceptionState is Microsoft . Scripting . KeyboardInterruptException ) Thread . ResetAbort ( ) ;
75+ objectName = string . Empty ;
76+ memberName = lastWord ;
7977 }
80- catch
78+
79+ Type type = TryGetType ( objectName ) ;
80+
81+ // Use Reflection for everything except in-built Python types and COM pbjects.
82+ if ( type != null && type . Namespace != "IronPython.Runtime" && ! type . FullName . Contains ( "IronPython.NewTypes" ) && ( type . Name != "__ComObject" ) )
8183 {
82- // Do nothing.
84+ PopulateFromCLRType ( items , type , objectName ) ;
8385 }
84- commandLine . ScriptScope . Engine . Runtime . IO . SetOutput ( stream , Encoding . UTF8 ) ;
85- AutocompletionInProgress = false ;
86+ else
87+ {
88+ PopulateFromPythonType ( items , objectName ) ;
89+ AutocompletionInProgress = false ;
90+ }
91+ }
92+ catch ( ThreadAbortException tae )
93+ {
94+ if ( tae . ExceptionState is Microsoft . Scripting . KeyboardInterruptException ) Thread . ResetAbort ( ) ;
95+ }
96+ catch
97+ {
98+ // Do nothing.
8699 }
87- return items . ToArray ( ) ;
100+ commandLine . ScriptScope . Engine . Runtime . IO . SetOutput ( stream , Encoding . UTF8 ) ;
101+ AutocompletionInProgress = false ;
102+ return Tuple . Create ( items . Cast < ICompletionData > ( ) . ToArray ( ) , objectName , memberName ) ;
88103 }
89104
90105 protected Type TryGetType ( string name )
@@ -141,6 +156,24 @@ protected void PopulateFromCLRType(List<PythonCompletionData> items, Type type,
141156 }
142157 }
143158
159+ protected void PopulateFromPythonType ( List < PythonCompletionData > items , string name )
160+ {
161+ //string dirCommand = "dir(" + objectName + ")";
162+ string dirCommand = "sorted([m for m in dir(" + name + ") if not m.startswith('__')], key = str.lower) + sorted([m for m in dir(" + name + ") if m.startswith('__')])" ;
163+ object value = commandLine . ScriptScope . Engine . CreateScriptSourceFromString ( dirCommand , SourceCodeKind . Expression ) . Execute ( commandLine . ScriptScope ) ;
164+ foreach ( object member in ( value as IronPython . Runtime . List ) )
165+ {
166+ bool isInstance = false ;
167+
168+ if ( name == string . Empty ) // Special case for globals
169+ {
170+ isInstance = TryGetType ( ( string ) member ) != null ;
171+ }
172+
173+ items . Add ( new PythonCompletionData ( ( string ) member , name , commandLine , isInstance ) ) ;
174+ }
175+ }
176+
144177 /// <summary>
145178 /// Generates completion data for the specified text. The text should be everything before
146179 /// the dot character that triggered the completion. The text can contain the command line prompt
@@ -160,8 +193,30 @@ public void GenerateDescription(string stub, string item, DescriptionUpdateDeleg
160193 //object value = commandLine.ScriptScope.Engine.CreateScriptSourceFromString(item, SourceCodeKind.Expression).Execute(commandLine.ScriptScope);
161194 //description = commandLine.ScriptScope.Engine.Operations.GetDocumentation(value);
162195 string docCommand = "" ;
163- if ( isInstance ) docCommand = "type(" + stub + ")" + "." + item + ".__doc__" ;
164- else docCommand = stub + "." + item + ".__doc__" ;
196+
197+ if ( isInstance )
198+ {
199+ if ( stub != string . Empty )
200+ {
201+ docCommand = "type(" + stub + ")" + "." + item + ".__doc__" ;
202+ }
203+ else
204+ {
205+ docCommand = "type(" + item + ")" + ".__doc__" ;
206+ }
207+ }
208+ else
209+ {
210+ if ( stub != string . Empty )
211+ {
212+ docCommand = stub + "." + item + ".__doc__" ;
213+ }
214+ else
215+ {
216+ docCommand = item + ".__doc__" ;
217+ }
218+ }
219+
165220 object value = commandLine . ScriptScope . Engine . CreateScriptSourceFromString ( docCommand , SourceCodeKind . Expression ) . Execute ( commandLine . ScriptScope ) ;
166221 description = ( string ) value ;
167222 AutocompletionInProgress = false ;
@@ -181,13 +236,111 @@ public void GenerateDescription(string stub, string item, DescriptionUpdateDeleg
181236 }
182237 }
183238
239+ private static readonly Regex MATCH_ALL_WORD = new Regex ( @"^\w+$" ) ;
240+ private static readonly Regex MATCH_LAST_WORD = new Regex ( @"\w+$" ) ;
241+
242+ private static readonly char [ ] DELIMITING_CHARS = { ',' , '\t ' , ' ' , ':' , ';' , '+' , '-' , '=' , '*' , '/' , '&' , '|' , '^' , '%' , '~' , '<' , '>' } ;
184243
185- string GetName ( string text )
244+ static string GetLastWord ( string text )
186245 {
187- text = text . Replace ( "\t " , " " ) ;
188- int startIndex = text . LastIndexOf ( ' ' ) ;
189- return text . Substring ( startIndex + 1 ) . Trim ( '.' ) ;
246+ return MATCH_LAST_WORD . Match ( text ) . Value ;
190247 }
191248
249+ static int FindLastDelimiter ( string text )
250+ {
251+ int lastDelimitingIndex = - 1 ;
252+
253+ // TODO: handle balanced but malformed cases such as '( [ ) ]'
254+ int lastUnbalancedParenthesisIndex = FindLastUnbalancedChar ( text , '(' , ')' ) ;
255+ int lastUnbalancedBracketIndex = FindLastUnbalancedChar ( text , '[' , ']' ) ;
256+
257+ lastDelimitingIndex = System . Math . Max ( lastUnbalancedParenthesisIndex , lastUnbalancedBracketIndex ) ;
258+
259+ bool insideDoubleQuotedString = false ;
260+ bool insideSingleQuotedString = false ;
261+
262+ for ( int i = ( lastDelimitingIndex + 1 ) ; i < text . Length ; i ++ )
263+ {
264+ char c = text [ i ] ;
265+
266+ // NOTE: rudimentary string detection (doesn't handle escaped quotes or triple quotes!)
267+ if ( c == '"' && ! insideSingleQuotedString )
268+ {
269+ insideDoubleQuotedString = ! insideDoubleQuotedString ;
270+ }
271+ else if ( c == '\' ' && ! insideDoubleQuotedString )
272+ {
273+ insideSingleQuotedString = ! insideSingleQuotedString ;
274+ }
275+ else if ( ! insideDoubleQuotedString && ! insideSingleQuotedString )
276+ {
277+ if ( c == '(' )
278+ {
279+ int lastClosed = FindLastUnbalancedChar ( text . Substring ( i + 1 ) , '(' , ')' ) ;
280+ i = i + 1 + lastClosed ;
281+ }
282+ else if ( c == '[' )
283+ {
284+ int lastClosed = FindLastUnbalancedChar ( text . Substring ( i + 1 ) , '[' , ']' ) ;
285+ i = i + 1 + lastClosed ;
286+ }
287+ else if ( DELIMITING_CHARS . Contains ( c ) )
288+ {
289+ lastDelimitingIndex = i ;
290+ }
291+ }
292+ }
293+
294+ return lastDelimitingIndex ;
295+ }
296+
297+ static int FindLastUnbalancedChar ( string text , char openedChar , char closedChar )
298+ {
299+ int lastIndex = - 1 ;
300+
301+ bool insideDoubleQuotedString = false ;
302+ bool insideSingleQuotedString = false ;
303+ var unbalancedIndices = new Stack < int > ( ) ;
304+
305+ for ( int i = 0 ; i < text . Length ; i ++ )
306+ {
307+ char c = text [ i ] ;
308+
309+ // NOTE: rudimentary string detection (doesn't handle escaped quotes or triple quotes!)
310+ if ( c == '"' && ! insideSingleQuotedString )
311+ {
312+ insideDoubleQuotedString = ! insideDoubleQuotedString ;
313+ }
314+ else if ( c == '\' ' && ! insideDoubleQuotedString )
315+ {
316+ insideSingleQuotedString = ! insideSingleQuotedString ;
317+ }
318+ else if ( ! insideDoubleQuotedString && ! insideSingleQuotedString )
319+ {
320+ if ( c == openedChar )
321+ {
322+ unbalancedIndices . Push ( i ) ;
323+ }
324+ else if ( c == closedChar )
325+ {
326+ if ( unbalancedIndices . Count == 0 )
327+ {
328+ lastIndex = i ;
329+ }
330+ else
331+ {
332+ unbalancedIndices . Pop ( ) ;
333+ }
334+ }
335+ }
336+ }
337+
338+ if ( unbalancedIndices . Count > 0 )
339+ {
340+ lastIndex = unbalancedIndices . Pop ( ) ;
341+ }
342+
343+ return lastIndex ;
344+ }
192345 }
193346}
0 commit comments