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

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy