Skip to content

Commit 32fb919

Browse files
committed
Scintilla sync callbacks
editor.callbackSync() adds a synchronous callback for Scintilla. Scintilla (at least as of version 3.3.9) runs the callbacks iterating on a vector::iterator. If the callback causes a change in the document watchers vectors then the iterator is no longer valid. The BoostRegexSearch uses a document watcher have a nice replace UX, so searchInTarget and findText are affected. Any changing of doc pointers or creating new documents also causes changes to the watchers. To cope with this, these operations are explicitly disallowed in synchronous callbacks. DepthCounter is used to keep track of if we are currently in a callback or not.
1 parent 02055aa commit 32fb919

27 files changed

Lines changed: 1434 additions & 920 deletions

PythonScript.Tests/PythonScript.Tests.vcxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
<ClInclude Include="targetver.h" />
135135
</ItemGroup>
136136
<ItemGroup>
137+
<ClCompile Include="..\PythonScript\src\DepthCounter.cpp" />
137138
<ClCompile Include="..\PythonScript\src\Replacer.cpp" />
138139
<ClCompile Include="..\PythonScript\src\UTF8Iterator.cpp" />
139140
<ClCompile Include="..\PythonScript\src\UtfConversion.cpp" />
@@ -143,6 +144,7 @@
143144
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='PythonDebug|Win32'">Create</PrecompiledHeader>
144145
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
145146
</ClCompile>
147+
<ClCompile Include="tests\TestDepthCounter.cpp" />
146148
<ClCompile Include="tests\TestReplacer.cpp" />
147149
</ItemGroup>
148150
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />

PythonScript.Tests/PythonScript.Tests.vcxproj.filters

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,11 @@
5050
<ClCompile Include="..\PythonScript\src\UtfConversion.cpp">
5151
<Filter>Source Files\linkedCode</Filter>
5252
</ClCompile>
53+
<ClCompile Include="..\PythonScript\src\DepthCounter.cpp">
54+
<Filter>Source Files\linkedCode</Filter>
55+
</ClCompile>
56+
<ClCompile Include="tests\TestDepthCounter.cpp">
57+
<Filter>Source Files\tests</Filter>
58+
</ClCompile>
5359
</ItemGroup>
5460
</Project>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#include "stdafx.h"
2+
3+
4+
#include <gtest/gtest.h>
5+
#include "DepthCounter.h"
6+
7+
namespace NppPythonScript
8+
{
9+
10+
class DepthCounterTest : public ::testing::Test {
11+
virtual void SetUp() {
12+
13+
}
14+
};
15+
16+
TEST_F(DepthCounterTest, testInitialStateIsZero) {
17+
DepthCounter depthCounter;
18+
19+
int depth = depthCounter.getDepth();
20+
21+
ASSERT_EQ(depth, 0);
22+
}
23+
24+
TEST_F(DepthCounterTest, testIncreaseIncreasesDepth) {
25+
DepthCounter depthCounter;
26+
27+
DepthLevel depthLevel = depthCounter.increase();
28+
29+
int depth = depthCounter.getDepth();
30+
31+
ASSERT_EQ(depth, 1);
32+
}
33+
34+
TEST_F(DepthCounterTest, testDestructorReturnsDepthToZero) {
35+
DepthCounter depthCounter;
36+
37+
int insideBlockDepth, outsideBlockDepth;
38+
39+
{
40+
DepthLevel depthLevel = depthCounter.increase();
41+
insideBlockDepth = depthCounter.getDepth();
42+
} // call depthLevel destructors
43+
44+
outsideBlockDepth = depthCounter.getDepth();
45+
46+
EXPECT_EQ(insideBlockDepth, 1);
47+
ASSERT_EQ(outsideBlockDepth, 0);
48+
}
49+
50+
TEST_F(DepthCounterTest, testMultipleDepth) {
51+
DepthCounter depthCounter;
52+
53+
int insideBlock1, insideBlock2;
54+
int outsideBlock1, outsideBlock2;
55+
56+
{
57+
DepthLevel depthLevel = depthCounter.increase();
58+
insideBlock1 = depthCounter.getDepth();
59+
60+
{
61+
DepthLevel depthLevel2 = depthCounter.increase();
62+
insideBlock2 = depthCounter.getDepth();
63+
}
64+
outsideBlock2 = depthCounter.getDepth();
65+
66+
}
67+
outsideBlock1 = depthCounter.getDepth();
68+
69+
EXPECT_EQ(insideBlock1, 1);
70+
EXPECT_EQ(insideBlock2, 2);
71+
EXPECT_EQ(outsideBlock2, 1);
72+
EXPECT_EQ(outsideBlock1, 0);
73+
}
74+
75+
}

PythonScript/project/PythonScript2010.vcxproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ xcopy $(ProjectDir)..\python_tests\*.* "e:\notepadtest\unicode\plugins\config\py
241241
<ClCompile Include="..\src\AboutDialog2.cpp" />
242242
<ClCompile Include="..\src\ArgumentException.cpp" />
243243
<ClCompile Include="..\src\CallbackExecArgs.cpp" />
244+
<ClCompile Include="..\src\DepthCounter.cpp" />
244245
<ClCompile Include="..\src\ConfigFile.cpp" />
245246
<ClCompile Include="..\src\ConsoleDialog.cpp" />
246247
<ClCompile Include="..\src\DebugTrace.cpp" />
@@ -249,9 +250,11 @@ xcopy $(ProjectDir)..\python_tests\*.* "e:\notepadtest\unicode\plugins\config\py
249250
<ClCompile Include="..\src\GILManager.cpp" />
250251
<ClCompile Include="..\src\GroupNotFoundException.cpp" />
251252
<ClCompile Include="..\src\HelpController.cpp" />
253+
<ClCompile Include="..\src\MainThread.cpp" />
252254
<ClCompile Include="..\src\Match.cpp" />
253255
<ClCompile Include="..\src\MatchPython.cpp" />
254256
<ClCompile Include="..\src\MenuManager.cpp" />
257+
<ClCompile Include="..\src\NotAllowedInCallbackException.cpp" />
255258
<ClCompile Include="..\src\NotepadPlusWrapper.cpp" />
256259
<ClCompile Include="..\src\NotepadPython.cpp" />
257260
<ClCompile Include="..\src\NotSupportedException.cpp" />
@@ -264,6 +267,7 @@ xcopy $(ProjectDir)..\python_tests\*.* "e:\notepadtest\unicode\plugins\config\py
264267
<ClCompile Include="..\src\PythonScript.cpp" />
265268
<ClCompile Include="..\src\ReplacementContainer.cpp" />
266269
<ClCompile Include="..\src\Replacer.cpp" />
270+
<ClCompile Include="..\src\ScintillaCallbackCounter.cpp" />
267271
<ClCompile Include="..\src\ScintillaCells.cpp" />
268272
<ClCompile Include="..\src\ScintillaPython.cpp" />
269273
<ClCompile Include="..\src\ScintillaWrapper.cpp" />
@@ -286,6 +290,7 @@ xcopy $(ProjectDir)..\python_tests\*.* "e:\notepadtest\unicode\plugins\config\py
286290
<ClInclude Include="..\src\ANSIIterator.h" />
287291
<ClInclude Include="..\src\ArgumentException.h" />
288292
<ClInclude Include="..\src\CallbackExecArgs.h" />
293+
<ClInclude Include="..\src\DepthCounter.h" />
289294
<ClInclude Include="..\src\ConfigFile.h" />
290295
<ClInclude Include="..\src\ConsoleDialog.h" />
291296
<ClInclude Include="..\src\ConsoleInterface.h" />
@@ -297,10 +302,12 @@ xcopy $(ProjectDir)..\python_tests\*.* "e:\notepadtest\unicode\plugins\config\py
297302
<ClInclude Include="..\src\GroupNotFoundException.h" />
298303
<ClInclude Include="..\src\HelpController.h" />
299304
<ClInclude Include="..\src\IDAllocator.h" />
305+
<ClInclude Include="..\src\MainThread.h" />
300306
<ClInclude Include="..\src\Match.h" />
301307
<ClInclude Include="..\src\MatchPython.h" />
302308
<ClInclude Include="..\src\MenuManager.h" />
303309
<ClInclude Include="..\src\MutexHolder.h" />
310+
<ClInclude Include="..\src\NotAllowedInCallbackException.h" />
304311
<ClInclude Include="..\src\NotepadPlusBuffer.h" />
305312
<ClInclude Include="..\src\NotepadPlusWrapper.h" />
306313
<ClInclude Include="..\src\NotepadPython.h" />
@@ -317,6 +324,8 @@ xcopy $(ProjectDir)..\python_tests\*.* "e:\notepadtest\unicode\plugins\config\py
317324
<ClInclude Include="..\src\ReplaceEntry.h" />
318325
<ClInclude Include="..\src\ReplacementContainer.h" />
319326
<ClInclude Include="..\src\Replacer.h" />
327+
<ClInclude Include="..\src\ScintillaCallback.h" />
328+
<ClInclude Include="..\src\ScintillaCallbackCounter.h" />
320329
<ClInclude Include="..\src\ScintillaCells.h" />
321330
<ClInclude Include="..\src\ScintillaNotifications.h" />
322331
<ClInclude Include="..\src\ScintillaPython.h" />

PythonScript/project/PythonScript2010.vcxproj.filters

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@
157157
<ClCompile Include="..\src\CallbackExecArgs.cpp">
158158
<Filter>Source Files</Filter>
159159
</ClCompile>
160+
<ClCompile Include="..\src\MainThread.cpp">
161+
<Filter>Source Files</Filter>
162+
</ClCompile>
163+
<ClCompile Include="..\src\NotAllowedInCallbackException.cpp">
164+
<Filter>Source Files</Filter>
165+
</ClCompile>
166+
<ClCompile Include="..\src\DepthCounter.cpp">
167+
<Filter>Source Files</Filter>
168+
</ClCompile>
169+
<ClCompile Include="..\src\ScintillaCallbackCounter.cpp">
170+
<Filter>Source Files</Filter>
171+
</ClCompile>
160172
</ItemGroup>
161173
<ItemGroup>
162174
<ClInclude Include="..\src\AboutDialog.h">
@@ -351,6 +363,21 @@
351363
<ClInclude Include="..\python_tests\tests\NotepadCallbackTestCase.py">
352364
<Filter>PythonTests\Tests</Filter>
353365
</ClInclude>
366+
<ClInclude Include="..\src\ScintillaCallback.h">
367+
<Filter>Source Files</Filter>
368+
</ClInclude>
369+
<ClInclude Include="..\src\MainThread.h">
370+
<Filter>Header Files</Filter>
371+
</ClInclude>
372+
<ClInclude Include="..\src\NotAllowedInCallbackException.h">
373+
<Filter>Source Files</Filter>
374+
</ClInclude>
375+
<ClInclude Include="..\src\DepthCounter.h">
376+
<Filter>Header Files</Filter>
377+
</ClInclude>
378+
<ClInclude Include="..\src\ScintillaCallbackCounter.h">
379+
<Filter>Header Files</Filter>
380+
</ClInclude>
354381
</ItemGroup>
355382
<ItemGroup>
356383
<ResourceCompile Include="..\res\PythonScript.rc">

PythonScript/python_tests/tests/ScintillaCallbackTestCase.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class ScintillaCallbackTestCase(unittest.TestCase):
1919
def setUp(self):
2020
notepad.new()
2121
self.callbackCalled = False
22+
self.callbackResults = {}
2223

2324
def tearDown(self):
2425
notepad.clearCallbacks()
@@ -154,6 +155,58 @@ def test_remove_callback_via_method_and_notification(self):
154155
self.assertFalse(globalCallbackCalled)
155156
self.assertTrue(self.callbackCalled) # the second callback should still have been called
156157

158+
159+
160+
def test_sync_modified_callback(self):
161+
editor.write('start\r\n')
162+
editor.callbackSync(lambda a: self.callback_sync_modified(a), [SCINTILLANOTIFICATION.MODIFIED])
163+
editor.write('change\r\n')
164+
calledDirectly = self.callbackCalled
165+
text = editor.getText()
166+
self.assertEqual(self.callbackResults['text'], 'start\r\nchange\r\n')
167+
self.assertEqual(self.callbackResults['modifiedText'], 'change\r\n')
168+
self.assertEqual(text, 'start\r\nchange\r\n')
169+
self.assertEqual(calledDirectly, True)
170+
171+
def callback_sync_modified(self, args):
172+
if args['modificationType'] & 1 == 0: # ignore modifications that aren't SC_MOD_INSERTTEXT
173+
return
174+
self.callbackResults['text'] = editor.getText()
175+
self.callbackResults['modifiedText'] = args['text']
176+
self.callbackCalled = True
177+
178+
def test_sync_setsavepoint(self):
179+
editor.write('start\r\n')
180+
editor.callbackSync(lambda a: self.callback_sync_setsavepoint(a), [SCINTILLANOTIFICATION.SAVEPOINTREACHED])
181+
editor.setSavePoint()
182+
self.assertEqual(self.callbackResults['text'], 'start\r\nin change\r\n')
183+
184+
def callback_sync_setsavepoint(self, args):
185+
editor.write('in change\r\n')
186+
self.callbackResults['text'] = editor.getText()
187+
188+
def test_sync_disallowed_scintilla_method(self):
189+
editor.write('Hello world')
190+
editor.callbackSync(lambda a: self.callback_sync_disallowed_scintilla_method(a), [SCINTILLANOTIFICATION.SAVEPOINTREACHED])
191+
editor.setSavePoint()
192+
self.assertTrue(self.callbackCalled)
193+
194+
195+
def callback_sync_disallowed_scintilla_method(self, args):
196+
self.callbackCalled = True
197+
with self.assertRaisesRegexp(RuntimeError, "not allowed in a synchronous"):
198+
found = editor.findText(0, 0, editor.getLength(), 'Hello')
199+
200+
def test_sync_disallowed_notepad_method(self):
201+
editor.write('Hello world')
202+
editor.callbackSync(lambda a: self.callback_sync_disallowed_notepad_method(a), [SCINTILLANOTIFICATION.SAVEPOINTREACHED])
203+
editor.setSavePoint()
204+
self.assertTrue(self.callbackCalled)
205+
206+
def callback_sync_disallowed_notepad_method(self, args):
207+
self.callbackCalled = True
208+
with self.assertRaisesRegexp(RuntimeError, "not allowed in a synchronous"):
209+
scintilla = notepad.createScintilla()
157210

158211
def poll_for_callback(self, timeout = 0.5, interval = 0.1):
159212
while self.callbackCalled == False and timeout > 0:

PythonScript/python_tests/tests/ScintillaWrapperTestCase.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,16 @@ def test_scintillawrapper_void_void_int(self):
9999
# we'll grab the doc point of the current document, then create another scintilla, set it to the document, write text in it,
100100
# then grab the text from the main 'editor' Scintilla, which should be what we added in
101101
docPointer = editor.getDocPointer()
102+
notepad.outputDebugString('creating hidden scintilla\n')
102103
hiddenScintilla = notepad.createScintilla()
104+
notepad.outputDebugString('setting doc pointer in hidden scintilla\n');
103105
hiddenScintilla.setDocPointer(docPointer)
106+
notepad.outputDebugString('complete - set doc pointer in hidden scintilla\n');
104107
hiddenScintilla.write('hello world, from the other side')
105108
text = editor.getText()
109+
notepad.outputDebugString('about to destroy scintilla\n');
106110
notepad.destroyScintilla(hiddenScintilla)
111+
notepad.outputDebugString('destroyed scintilla\n');
107112
self.assertEqual(text, 'hello world, from the other side')
108113

109114
def callback_scintillawrapper_void_void_int(self, args):
@@ -113,6 +118,7 @@ def callback_scintillawrapper_void_void_int(self, args):
113118
self.callbackCalled = True
114119

115120
def test_scintillawrapper_void_void_int_in_callback(self):
121+
notepad.outputDebugString('test_scintillawrapper_void_void_int_in_callback')
116122
editor.write('test');
117123
editor.callback(lambda args: self.callback_scintillawrapper_void_void_int(args), [SCINTILLANOTIFICATION.SAVEPOINTREACHED])
118124
editor.setSavePoint()

0 commit comments

Comments
 (0)