Skip to content

Commit 7bf7ec5

Browse files
Backport PR #16682: Prevent replacing code with find and replace in read-only cells (#16696)
Co-authored-by: Vishnutheep B <vishnutheep@gmail.com>
1 parent 355cbd5 commit 7bf7ec5

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

packages/cells/src/searchprovider.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
GenericSearchProvider,
1313
IBaseSearchProvider,
1414
IFilters,
15+
IReplaceOptions,
1516
ISearchMatch
1617
} from '@jupyterlab/documentsearch';
1718
import { OutputArea } from '@jupyterlab/outputarea';
@@ -249,6 +250,39 @@ class CodeCellSearchProvider extends CellSearchProvider {
249250
}
250251
}
251252

253+
/**
254+
* Replace all matches in the cell source with the provided text
255+
*
256+
* @param newText The replacement text.
257+
* @returns Whether a replace occurred.
258+
*/
259+
async replaceAllMatches(newText: string): Promise<boolean> {
260+
if (this.model.getMetadata('editable') === false)
261+
return Promise.resolve(false);
262+
263+
const result = await super.replaceAllMatches(newText);
264+
return result;
265+
}
266+
267+
/**
268+
* Replace the currently selected match with the provided text.
269+
* If no match is selected, it won't do anything.
270+
*
271+
* @param newText The replacement text.
272+
* @returns Whether a replace occurred.
273+
*/
274+
async replaceCurrentMatch(
275+
newText: string,
276+
loop?: boolean,
277+
options?: IReplaceOptions
278+
): Promise<boolean> {
279+
if (this.model.getMetadata('editable') === false)
280+
return Promise.resolve(false);
281+
282+
const result = await super.replaceCurrentMatch(newText, loop, options);
283+
return result;
284+
}
285+
252286
private async _onOutputsChanged(
253287
outputArea: OutputArea,
254288
changes: number
@@ -393,12 +427,33 @@ class MarkdownCellSearchProvider extends CellSearchProvider {
393427
* @returns Whether a replace occurred.
394428
*/
395429
async replaceAllMatches(newText: string): Promise<boolean> {
430+
if (this.model.getMetadata('editable') === false)
431+
return Promise.resolve(false);
432+
396433
const result = await super.replaceAllMatches(newText);
397434
// if the cell is rendered force update
398435
if ((this.cell as MarkdownCell).rendered) {
399436
this.cell.update();
400437
}
438+
return result;
439+
}
440+
441+
/**
442+
* Replace the currently selected match with the provided text.
443+
* If no match is selected, it won't do anything.
444+
*
445+
* @param newText The replacement text.
446+
* @returns Whether a replace occurred.
447+
*/
448+
async replaceCurrentMatch(
449+
newText: string,
450+
loop?: boolean,
451+
options?: IReplaceOptions
452+
): Promise<boolean> {
453+
if (this.model.getMetadata('editable') === false)
454+
return Promise.resolve(false);
401455

456+
const result = await super.replaceCurrentMatch(newText, loop, options);
402457
return result;
403458
}
404459

packages/notebook/test/searchprovider.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,38 @@ describe('@jupyterlab/notebook', () => {
288288
expect(source).toBe('rabarbar');
289289
expect(provider.currentMatchIndex).toBe(null);
290290
});
291+
292+
it('should not replace the current match in a read-only cell', async () => {
293+
panel.model!.sharedModel.insertCells(0, [
294+
{
295+
cell_type: 'markdown',
296+
source: 'test1',
297+
metadata: { editable: false }
298+
},
299+
{ cell_type: 'code', source: 'test2', metadata: { editable: false } }
300+
]);
301+
302+
await provider.startQuery(/test\d/, undefined);
303+
expect(provider.currentMatchIndex).toBe(0);
304+
let replaced = await provider.replaceCurrentMatch('bar');
305+
expect(replaced).toBe(false);
306+
const source = panel.model!.cells.get(0).sharedModel.getSource();
307+
expect(source).toBe('test1');
308+
309+
await provider.highlightNext();
310+
expect(provider.currentMatchIndex).toBe(1);
311+
replaced = await provider.replaceCurrentMatch('bar');
312+
expect(replaced).toBe(false);
313+
const source1 = panel.model!.cells.get(1).sharedModel.getSource();
314+
expect(source1).toBe('test2');
315+
316+
await provider.highlightNext();
317+
expect(provider.currentMatchIndex).toBe(2);
318+
replaced = await provider.replaceCurrentMatch('bar');
319+
expect(replaced).toBe(true);
320+
const source2 = panel.model!.cells.get(2).sharedModel.getSource();
321+
expect(source2).toBe('bar test2');
322+
});
291323
});
292324

293325
describe('#replaceAllMatches()', () => {
@@ -354,6 +386,34 @@ describe('@jupyterlab/notebook', () => {
354386
expect(source).toBe('test1\nbar2\nbar3\nbar4\ntest5');
355387
await provider.endQuery();
356388
});
389+
390+
it('should not replace all matches in read-only cells', async () => {
391+
panel.model!.sharedModel.insertCells(2, [
392+
{
393+
cell_type: 'markdown',
394+
source: 'test1 test2',
395+
metadata: { editable: false }
396+
},
397+
{
398+
cell_type: 'code',
399+
source: 'test1 test2 test3',
400+
metadata: { editable: false }
401+
}
402+
]);
403+
await provider.startQuery(/test\d/, undefined);
404+
await provider.highlightNext();
405+
const replaced = await provider.replaceAllMatches('test0');
406+
expect(replaced).toBe(true);
407+
let source = panel.model!.cells.get(0).sharedModel.getSource();
408+
expect(source).toBe('test0 test0');
409+
source = panel.model!.cells.get(1).sharedModel.getSource();
410+
expect(source).toBe('test0');
411+
source = panel.model!.cells.get(2).sharedModel.getSource();
412+
expect(source).toBe('test1 test2');
413+
source = panel.model!.cells.get(3).sharedModel.getSource();
414+
expect(source).toBe('test1 test2 test3');
415+
expect(provider.currentMatchIndex).toBe(null);
416+
});
357417
});
358418

359419
describe('#getSelectionState()', () => {

0 commit comments

Comments
 (0)