Skip to content

Commit 94043d3

Browse files
authored
Merge pull request #59 from bvn-architecture/some_improvements
Improved auto-completion. Fix paste at cursor bug.
2 parents ff44eb2 + 50d3653 commit 94043d3

File tree

3 files changed

+237
-65
lines changed

3 files changed

+237
-65
lines changed

PythonConsoleControl/PythonConsole.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public bool DisableAutocompletionForCallables
5454
}
5555
}
5656

57-
bool allowCtrlSpaceAutocompletion = false;
57+
bool allowCtrlSpaceAutocompletion = true;
5858
public bool AllowCtrlSpaceAutocompletion
5959
{
6060
get { return allowCtrlSpaceAutocompletion; }
@@ -309,11 +309,11 @@ protected void OnPaste(object target, ExecutedRoutedEventArgs args)
309309
else if (rectangular && textArea.Selection.IsEmpty)
310310
{
311311
if (!RectangleSelection.PerformRectangularPaste(textArea, textArea.Caret.Position, text, false))
312-
textEditor.Write(text, false);
312+
textEditor.Write(text, false, false);
313313
}
314314
else
315315
{
316-
textEditor.Write(text, false);
316+
textEditor.Write(text, false, false);
317317
}
318318
}
319319
textArea.Caret.BringCaretToView();
@@ -571,7 +571,7 @@ void textEditor_TextEntering(object sender, TextCompositionEventArgs e)
571571
{
572572
if (e.Text.Length > 0)
573573
{
574-
if (!char.IsLetterOrDigit(e.Text[0]))
574+
if (!char.IsLetterOrDigit(e.Text[0]) || e.Text[0] == '_') // Underscore is a fairly common character in Revit API names.
575575
{
576576
// Whenever a non-letter is typed while the completion window is open,
577577
// insert the currently selected element.

PythonConsoleControl/PythonConsoleCompletionDataProvider.cs

Lines changed: 194 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.Scripting;
1414
using System.Threading;
1515
using System.Reflection;
16+
using System.Text.RegularExpressions;
1617

1718
namespace 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

Comments
 (0)