Skip to content

Commit

Permalink
DYN-6794 python script editor convert legacy tabs to spaces (#15179)
Browse files Browse the repository at this point in the history
* first draft

* convert tabs to spaces in script editor

- button to convert legacy tabs to space indents
- holds pace indentation strategy even in legacy code
- test
- clean
- button icon is just a placeholder

* StronglyTypedResourceBuilder back to 16

* update

---------

Co-authored-by: Aaron (Qilong) <[email protected]>
  • Loading branch information
ivaylo-matov and QilongTang authored May 6, 2024
1 parent 21a684e commit 574e805
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 13 deletions.
13 changes: 11 additions & 2 deletions src/Libraries/PythonNodeModels/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@
<data name="PythonScriptEditorEngineDropdownTooltip" xml:space="preserve">
<value>Select the Python version/engine to execute the script</value>
</data>
<data name="PythonScriptEditorConvertTabsToSpacesButtonTooltip" xml:space="preserve">
<value>Convert indentation tabs to spaces...</value>
</data>
<data name="PythonScriptEditorMigrationAssistantButtonTooltip" xml:space="preserve">
<value>Convert script to Python 3...</value>
</data>
Expand All @@ -214,4 +217,4 @@
<data name="PythonScriptUnsavedChangesPromptTitle" xml:space="preserve">
<value>Are you sure you want to leave?</value>
</data>
</root>
</root>
5 changes: 4 additions & 1 deletion src/Libraries/PythonNodeModels/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@
<data name="PythonScriptEditorMigrationAssistantButtonTooltip" xml:space="preserve">
<value>Convert script to Python 3...</value>
</data>
<data name="PythonScriptEditorConvertTabsToSpacesButtonTooltip" xml:space="preserve">
<value>Convert indentation tabs to spaces...</value>
</data>
<data name="PythonSearchTags" xml:space="preserve">
<value>IronPython;CPython;</value>
</data>
Expand All @@ -215,4 +218,4 @@
<data name="PythonScriptUnsavedChangesPromptTitle" xml:space="preserve">
<value>Are you sure you want to leave?</value>
</data>
</root>
</root>
39 changes: 31 additions & 8 deletions src/Libraries/PythonNodeModelsWpf/PythonIndentationStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal class PythonIndentationStrategy : DefaultIndentationStrategy
{
#region Fields

const int indent_space_count = 4;
const int IndentSpaceCount = 4;

TextEditor textEditor;

Expand Down Expand Up @@ -55,7 +55,7 @@ public override void IndentLine(TextDocument document, DocumentLine line)
// We should indent
else if (prevLine.EndsWith(":") && !previousIsComment)
{
var ind = new string(' ', prev + indent_space_count);
var ind = new string(' ', prev + IndentSpaceCount);
document.Insert(line.Offset, ind);
}
else
Expand All @@ -66,17 +66,40 @@ public override void IndentLine(TextDocument document, DocumentLine line)
}
}

// Calculates the amount of white space leading in a string
/// <summary>
/// Calculates the total width of leading whitespace in a string, where each space (' ') counts as 1
/// and each tab ('\t') counts as 4. Ensures consistent behavior for indentation and folding strategy
/// when transitioning from legacy code with tab indentations to modern conventions using spaces.
/// </summary>
private int CalcSpace(string str)
{
int count = 0;
for (int i = 0; i < str.Length; ++i)
{
if (!char.IsWhiteSpace(str[i]))
return i;
if (i == str.Length - 1)
return str.Length;
if (str[i] == ' ')
{
count += 1;
}
else if (str[i] == '\t')
{
count += 4;
}
else if (!char.IsWhiteSpace(str[i]))
{
return count;
}
}
return 0;
return count;
}

/// <summary>
/// Converts tabs to spaces in a legacy python code.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static string ConvertTabsToSpaces(string text)
{
return text.Replace("\t", new string(' ', IndentSpaceCount));
}
}
}
4 changes: 4 additions & 0 deletions src/Libraries/PythonNodeModelsWpf/PythonNodeModelsWpf.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
<None Remove="Resources\reset_hover.png" />
<None Remove="Resources\save.png" />
<None Remove="Resources\save_hover.png" />
<None Remove="Resources\tabs.png" />
<None Remove="Resources\tabs_hover.png" />
<None Remove="Resources\undo.png" />
<None Remove="Resources\undo_hover.png" />
<None Remove="Resources\zoom-in.png" />
Expand Down Expand Up @@ -85,6 +87,8 @@
<Resource Include="Resources\reset_hover.png" />
<Resource Include="Resources\save.png" />
<Resource Include="Resources\save_hover.png" />
<Resource Include="Resources\tabs.png" />
<Resource Include="Resources\tabs_hover.png" />
<Resource Include="Resources\undo.png" />
<Resource Include="Resources\undo_hover.png" />
<Resource Include="Resources\zoom-in.png" />
Expand Down
Binary file added src/Libraries/PythonNodeModelsWpf/Resources/tabs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions src/Libraries/PythonNodeModelsWpf/ScriptEditorWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,22 @@
Source="/PythonNodeModelsWpf;component/Resources/convert_hover.png" />
</Button.Resources>
</Button>
<Button Style="{StaticResource IconButton}"
Name="ConvertTabsToSpacesButton"
Click="OnConvertTabsToSpacesClicked"
ToolTipService.ShowOnDisabled="True"
ToolTipService.IsEnabled="True"
IsEnabled="True">
<Button.ToolTip>
<ToolTip Content="{x:Static p:Resources.PythonScriptEditorConvertTabsToSpacesButtonTooltip}" Style="{StaticResource GenericToolTipLight}"/>
</Button.ToolTip>
<Button.Resources>
<Image x:Key="Shape"
Source="/PythonNodeModelsWpf;component/Resources/tabs.png" />
<Image x:Key="HighlightShape"
Source="/PythonNodeModelsWpf;component/Resources/tabs_hover.png" />
</Button.Resources>
</Button>
<Button Style="{StaticResource IconButton}"
Name="MoreInfoButton"
Click="OnMoreInfoClicked">
Expand Down
13 changes: 13 additions & 0 deletions src/Libraries/PythonNodeModelsWpf/ScriptEditorWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,17 @@ private void OnMigrationAssistantClicked(object sender, RoutedEventArgs e)
Dynamo.Logging.Categories.PythonOperations);
NodeModel.RequestCodeMigration(e);
}
private void OnConvertTabsToSpacesClicked(object sender, RoutedEventArgs e)
{
if (NodeModel == null)
throw new NullReferenceException(nameof(NodeModel));

if (editText.Document != null && !String.IsNullOrEmpty(editText.Document.Text))
{
var convertedText = PythonIndentationStrategy.ConvertTabsToSpaces(editText.Document.Text);
editText.Document.Text = convertedText;
}
}

private void OnMoreInfoClicked(object sender, RoutedEventArgs e)
{
Expand Down Expand Up @@ -678,6 +689,7 @@ private void WarnUserScript()
this.ZoomOutButton.IsEnabled = false;
this.EngineSelectorComboBox.IsEnabled = false;
this.MigrationAssistantButton.IsEnabled = false;
this.ConvertTabsToSpacesButton.IsEnabled = false;
this.MoreInfoButton.IsEnabled = false;
this.SaveButtonBar.Visibility = Visibility.Collapsed;
this.UnsavedChangesStatusBar.Visibility = Visibility.Visible;
Expand All @@ -695,6 +707,7 @@ private void ResumeButton_OnClick(object sender, RoutedEventArgs e)
this.ZoomOutButton.IsEnabled = true;
this.EngineSelectorComboBox.IsEnabled = true;
this.MigrationAssistantButton.IsEnabled = true;
this.ConvertTabsToSpacesButton.IsEnabled = true;
this.MoreInfoButton.IsEnabled = true;
this.SaveButtonBar.Visibility = Visibility.Visible;
this.UnsavedChangesStatusBar.Visibility = Visibility.Collapsed;
Expand Down
48 changes: 47 additions & 1 deletion test/Libraries/DynamoPythonTests/PythonEditTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Dynamo.PythonServices;
using DynCmd = Dynamo.Models.DynamoModel;
using System.Threading;
using PythonNodeModelsWpf;

namespace Dynamo.Tests
{
Expand Down Expand Up @@ -68,6 +69,22 @@ private void UpdatePythonNodeContent(ModelBase pythonNode, string value)
ViewModel.ExecuteCommand(command);
}

/// <summary>
/// Counts the non-overlapping occurrences of a specified substring within a given string.
/// </summary>
private int CountSubstrings(string code, string subscting)
{
int count = 0;
int index = code.IndexOf(subscting, 0);

while (index != -1)
{
count++;
index = code.IndexOf(subscting, index + subscting.Length);
}
return count;
}

[Test]
public void PythonScriptEdit_WorkspaceChangesReflected()
{
Expand Down Expand Up @@ -202,6 +219,36 @@ public void PythonScriptEdit_UndoRedo()
Assert.AreEqual(pynode.Script, newScript);
}

[Test]
public void PythonScriptEdit_ConvertTabsToSpacesButton()
{
// Open file and get the Python node
var model = ViewModel.Model;
var examplePath = Path.Combine(TestDirectory, @"core\python", "ConvertTabsToSpaces.dyn");
ViewModel.OpenCommand.Execute(examplePath);
var pynode = model.CurrentWorkspace.Nodes.OfType<PythonNode>().First();

// Asset the node is loaded
Assert.NotNull(pynode, "Python node should be loaded from the file.");

// number of spaces is hard coded as providing a public property or changing the access
// level of PythonIndentationStrategy.ConvertTabsToSpaces is unnecessary for this purpose only
var spacesIndent = new string(' ', 4);
var tabIndent = "\t";

// Assert initial conditions : 17 tab indents and no space indents
Assert.IsTrue(pynode.Script.Count(c => c == '\t') == 17);
Assert.IsTrue(CountSubstrings(pynode.Script, spacesIndent) == 0);

// Convert tabs to spaces
var convertedString = PythonIndentationStrategy.ConvertTabsToSpaces(pynode.Script);
pynode.Script = convertedString;

// Assert the tab indents are converted to space indents
Assert.IsTrue(pynode.Script.Count(c => c == '\t') == 0);
Assert.IsTrue(CountSubstrings(pynode.Script, spacesIndent) == 17);
}

[Test]
public void VarInPythonScriptEdit_WorkspaceChangesReflected()
{
Expand Down Expand Up @@ -448,7 +495,6 @@ public void TestWorkspaceWithMultiplePythonEngines()
}

[Test]

public void Python_CanReferenceDynamoServicesExecutionSession()
{
// open test graph
Expand Down
106 changes: 106 additions & 0 deletions test/core/python/ConvertTabsToSpaces.dyn
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
"Uuid": "9cb0c7b0-bfa6-40a6-9d21-1fdfb4b329e0",
"IsCustomNode": false,
"Description": "",
"Name": "ConvertTabsToSpaces",
"ElementResolver": {
"ResolutionMap": {}
},
"Inputs": [],
"Outputs": [],
"Nodes": [
{
"ConcreteType": "PythonNodeModels.PythonNode, PythonNodeModels",
"Code": "# declare x\r\nx = 7\r\noutcome = \"\"\r\n\r\n# random statements\r\nif x == 5:\r\n\toutcome = \"x is 5\"\r\n\tif x == 9:\r\n\t\tif x ==10:\r\n\t\t\toutcome = \"impossible\"\r\n\telse:\r\n\t\toutcome = \"x is not 5\"\r\n\t\tif x == 1:\r\n\t\t\t# tab\tin\tcomment",
"Engine": "CPython3",
"VariableInputPorts": true,
"Id": "0894436c129744c3b772a4c9d44c329e",
"NodeType": "PythonScriptNode",
"Inputs": [
{
"Id": "8422c558b614406fbbb45f2ba0394149",
"Name": "IN[0]",
"Description": "Input #0",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
}
],
"Outputs": [
{
"Id": "f871bd7ad08648a6b6cc660024641eb1",
"Name": "OUT",
"Description": "Result of the python script",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
}
],
"Replication": "Disabled",
"Description": "Runs an embedded Python script."
}
],
"Connectors": [],
"Dependencies": [],
"NodeLibraryDependencies": [],
"EnableLegacyPolyCurveBehavior": true,
"Thumbnail": "",
"GraphDocumentationURL": null,
"ExtensionWorkspaceData": [
{
"ExtensionGuid": "28992e1d-abb9-417f-8b1b-05e053bee670",
"Name": "Properties",
"Version": "3.1",
"Data": {}
}
],
"Author": "",
"Linting": {
"activeLinter": "None",
"activeLinterId": "7b75fb44-43fd-4631-a878-29f4d5d8399a",
"warningCount": 0,
"errorCount": 0
},
"Bindings": [],
"View": {
"Dynamo": {
"ScaleFactor": 1.0,
"HasRunWithoutCrash": true,
"IsVisibleInDynamoLibrary": true,
"Version": "3.1.0.3411",
"RunType": "Manual",
"RunPeriod": "1000"
},
"Camera": {
"Name": "_Background Preview",
"EyeX": -17.0,
"EyeY": 24.0,
"EyeZ": 50.0,
"LookX": 12.0,
"LookY": -13.0,
"LookZ": -58.0,
"UpX": 0.0,
"UpY": 1.0,
"UpZ": 0.0
},
"ConnectorPins": [],
"NodeViews": [
{
"Id": "0894436c129744c3b772a4c9d44c329e",
"Name": "Python Script",
"IsSetAsInput": false,
"IsSetAsOutput": false,
"Excluded": false,
"ShowGeometry": true,
"X": 323.0,
"Y": 217.0
}
],
"Annotations": [],
"X": -266.0446043165468,
"Y": -33.539568345323744,
"Zoom": 0.9165467625899281
}
}

0 comments on commit 574e805

Please sign in to comment.