Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DYN-6794 python script editor convert legacy tabs to spaces #15179

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
reddyashish marked this conversation as resolved.
Show resolved Hide resolved
{
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)
reddyashish marked this conversation as resolved.
Show resolved Hide resolved
{
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
}
}
Loading