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

Add search for output rich content in notebooks #173978

Merged
merged 53 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f895929
initial marks
andreamah Oct 19, 2022
03ad467
initial changes to support notebook editor binding
andreamah Oct 21, 2022
cc1a778
change notebook track qualifications
andreamah Nov 8, 2022
6bba961
needs to read into dom
andreamah Nov 15, 2022
75dddd2
random logs
andreamah Nov 17, 2022
b7f40ea
some progress on notebook search
andreamah Nov 22, 2022
ac5a174
Improve workspace text search with notebook editors
andreamah Nov 22, 2022
90763cc
some rendered content showing in search viewlet
andreamah Nov 23, 2022
e37722e
rough draft of notebook search
andreamah Nov 30, 2022
d20ea1d
move local notebook search out of searchservice
andreamah Dec 2, 2022
188ee99
working replace
andreamah Jan 17, 2023
51c667e
add propert highlighting and open notebook editor when replace active
andreamah Jan 19, 2023
65337b5
merge main
andreamah Jan 19, 2023
da22b6a
cleanup and compile error fixing
andreamah Jan 19, 2023
779e721
fix function for notebook opening in search
andreamah Jan 19, 2023
b8c5133
clarity on shouldOpenInNotebookEditor
andreamah Jan 19, 2023
15ea8bc
added experimental flag
andreamah Jan 20, 2023
310c13a
extra docs for notebook extractSelectionLine
andreamah Jan 20, 2023
a782747
fix experimental flag bug
andreamah Jan 20, 2023
f262935
resolve merge
andreamah Jan 20, 2023
eff7d26
incorporated shared findMatchDecorationModel
andreamah Jan 21, 2023
b1e69a1
jump to nth result on raw text results to notebook results
andreamah Jan 23, 2023
136438c
Merge branch 'main' into andreamah/issue148068
andreamah Feb 1, 2023
7b97332
fix bug with focus shift
andreamah Feb 7, 2023
349b100
cleanup
andreamah Feb 7, 2023
d11d169
fix tests and cleanup
andreamah Feb 8, 2023
4cd6107
extra cleanup for match decorations
andreamah Feb 8, 2023
5409745
cleanup diffs
andreamah Feb 8, 2023
a368db1
remove comments and add readonly field
andreamah Feb 8, 2023
917ac78
remove webview extract match for now
andreamah Feb 9, 2023
aa79ffc
remove comma
andreamah Feb 9, 2023
41b21f3
Revert "remove comma"
andreamah Feb 9, 2023
716a8d0
Revert "remove webview extract match for now"
andreamah Feb 9, 2023
ff55730
Merge branch 'main' into andreamah/notebook-rich-content-output
andreamah Feb 13, 2023
4c23aac
some support for output search
andreamah Feb 14, 2023
244e4a1
work on repalce visibility
andreamah Feb 15, 2023
655c4bb
basic output working
andreamah Feb 16, 2023
cceee76
Merge branch 'main' into andreamah/notebook-rich-content-output
andreamah Mar 6, 2023
821e0fc
add options to notebook find widget to show/hide filter
andreamah Mar 7, 2023
8ff2b29
cleanup
andreamah Mar 8, 2023
ee05e1f
fix rendered markdown not resolving and first rendered markdown openi…
andreamah Mar 8, 2023
9696d2c
remove commented out code
andreamah Mar 8, 2023
8059cef
remove filterStartVisibliity from findInput options since it's only f…
andreamah Mar 8, 2023
652cc51
add todo
andreamah Mar 8, 2023
4aae125
add another todo
andreamah Mar 8, 2023
a4d525c
refresh -> reload
andreamah Mar 10, 2023
9d652d7
isolate notebook find filtering into its own class
andreamah Mar 11, 2023
5a7209d
notebook find filter isolation cleanup
andreamah Mar 13, 2023
630a1ee
refactoring SearchFindInput
andreamah Mar 15, 2023
e6e734b
remove changes to notebookFindReplaceWidget.css
andreamah Mar 15, 2023
a9fa720
Merge branch 'main' into andreamah/notebook-rich-content-output
andreamah Mar 15, 2023
5192011
NotebookFindInputFilter -> NotebookFindInputFilterButton
andreamah Mar 15, 2023
f6b2a06
Merge branch 'main' into andreamah/notebook-rich-content-output
andreamah Mar 17, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { defaultInputBoxStyles, defaultProgressBarStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles';
import { IToggleStyles } from 'vs/base/browser/ui/toggle/toggle';
import { Disposable } from 'vs/base/common/lifecycle';

const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find");
const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find");
Expand Down Expand Up @@ -150,31 +151,26 @@ class NotebookFindFilterActionViewItem extends DropdownMenuActionViewItem {
}
}

class NotebookFindInput extends FindInput {
export class NotebookFindInputFilter extends Disposable {
private _filterButtonContainer: HTMLElement;
rebornix marked this conversation as resolved.
Show resolved Hide resolved
private _actionbar: ActionBar | null = null;
private _filterChecked: boolean = false;
private _filtersAction: IAction;
private _toggleStyles: IToggleStyles;

constructor(
readonly filters: NotebookFindFilters,
contextKeyService: IContextKeyService,
readonly contextMenuService: IContextMenuService,
readonly instantiationService: IInstantiationService,
parent: HTMLElement | null,
contextViewProvider: IContextViewProvider,
options: IFindInputOptions
options: IFindInputOptions,
tooltip: string = NOTEBOOK_FIND_FILTERS,
) {
super(parent, contextViewProvider, options);

super();
this._toggleStyles = options.toggleStyles;

this._register(registerAndCreateHistoryNavigationContext(contextKeyService, this.inputBox));
this._filtersAction = new Action('notebookFindFilterAction', NOTEBOOK_FIND_FILTERS, 'notebook-filters ' + ThemeIcon.asClassName(filterIcon));
this._filtersAction = new Action('notebookFindFilterAction', tooltip, 'notebook-filters ' + ThemeIcon.asClassName(filterIcon));
this._filtersAction.checked = false;
this._filterButtonContainer = dom.$('.find-filter-button');
this.controls.appendChild(this._filterButtonContainer);
this.createFilters(this._filterButtonContainer);

this._register(this.filters.onDidChange(() => {
Expand All @@ -184,14 +180,24 @@ class NotebookFindInput extends FindInput {
this._filtersAction.checked = false;
}
}));
}

this.inputBox.paddingRight = (this.caseSensitive?.width() ?? 0) + (this.wholeWords?.width() ?? 0) + (this.regex?.width() ?? 0) + this.getFilterWidth();
get container() {
return this._filterButtonContainer;
}

private getFilterWidth() {
get width() {
return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */;
}

applyStyles(filterChecked: boolean): void {
const toggleStyles = this._toggleStyles;

this._filterButtonContainer.style.borderColor = (filterChecked && toggleStyles.inputActiveOptionBorder) || '';
this._filterButtonContainer.style.color = (filterChecked && toggleStyles.inputActiveOptionForeground) || 'inherit';
this._filterButtonContainer.style.backgroundColor = (filterChecked && toggleStyles.inputActiveOptionBackground) || '';
}

private createFilters(container: HTMLElement): void {
this._actionbar = this._register(new ActionBar(container, {
actionViewItemProvider: action => {
Expand All @@ -203,6 +209,29 @@ class NotebookFindInput extends FindInput {
}));
this._actionbar.push(this._filtersAction, { icon: true, label: false });
}
}

export class NotebookFindInput extends FindInput {
private _findFilter: NotebookFindInputFilter;
private _filterChecked: boolean = false;
rebornix marked this conversation as resolved.
Show resolved Hide resolved

constructor(
readonly filters: NotebookFindFilters,
contextKeyService: IContextKeyService,
readonly contextMenuService: IContextMenuService,
readonly instantiationService: IInstantiationService,
parent: HTMLElement | null,
contextViewProvider: IContextViewProvider,
options: IFindInputOptions,
) {
super(parent, contextViewProvider, options);

this._register(registerAndCreateHistoryNavigationContext(contextKeyService, this.inputBox));
this._findFilter = this._register(new NotebookFindInputFilter(filters, contextMenuService, instantiationService, options));

this.inputBox.paddingRight = (this.caseSensitive?.width() ?? 0) + (this.wholeWords?.width() ?? 0) + (this.regex?.width() ?? 0) + this._findFilter.width;
this.controls.appendChild(this._findFilter.container);
}

override setEnabled(enabled: boolean) {
super.setEnabled(enabled);
Expand All @@ -226,15 +255,7 @@ class NotebookFindInput extends FindInput {
this.regex.domNode.classList.toggle('disabled', false);
}
}
this.applyStyles();
}

private applyStyles(): void {
const toggleStyles = this._toggleStyles;

this._filterButtonContainer.style.borderColor = (this._filterChecked && toggleStyles.inputActiveOptionBorder) || '';
this._filterButtonContainer.style.color = (this._filterChecked && toggleStyles.inputActiveOptionForeground) || 'inherit';
this._filterButtonContainer.style.backgroundColor = (this._filterChecked && toggleStyles.inputActiveOptionBackground) || '';
this._findFilter.applyStyles(this._filterChecked);
}

getCellToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IAction[] } {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2415,7 +2415,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
return Promise.all(requests);
}

async find(query: string, options: INotebookSearchOptions, token: CancellationToken, skipWarmup: boolean = false): Promise<CellFindMatchWithIndex[]> {
async find(query: string, options: INotebookSearchOptions, token: CancellationToken, skipWarmup: boolean = false, shouldGetSearchPreviewInfo = false): Promise<CellFindMatchWithIndex[]> {
if (!this._notebookViewModel) {
return [];
}
Expand Down Expand Up @@ -2445,7 +2445,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
if (this._webview) {
// request all outputs to be rendered
await this._warmupAll(!!options.includeOutput);
const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput });
const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput, shouldGetSearchPreviewInfo });
// attach webview matches to model find matches
webviewMatches.forEach(match => {
if (!options.includeMarkupPreview && match.type === 'preview') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1454,7 +1454,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
});
}

async find(query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean }): Promise<IFindMatch[]> {
async find(query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean }): Promise<IFindMatch[]> {
rebornix marked this conversation as resolved.
Show resolved Hide resolved
if (query === '') {
return [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export interface ITokenizedStylesChangedMessage {
export interface IFindMessage {
readonly type: 'find';
readonly query: string;
readonly options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean };
readonly options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean };
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -829,9 +829,18 @@ async function webviewPreloads(ctx: PreloadContext) {
container: Node;
originalRange: Range;
isShadow: boolean;
searchPreviewInfo?: ISearchPreviewInfo;
highlightResult?: IHighlightResult;
}

interface ISearchPreviewInfo {
line: string;
range: {
start: number;
end: number;
};
}

interface IHighlighter {
highlightCurrentMatch(index: number): void;
unHighlightCurrentMatch(index: number): void;
Expand Down Expand Up @@ -956,7 +965,82 @@ async function webviewPreloads(ctx: PreloadContext) {
}
}

const find = (query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean }) => {
function extractSelectionLine(selection: Selection): ISearchPreviewInfo {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rebornix

extractSelectionLine can be costly, I wonder if we want to request this info conditionally (when requested from the search widget). Also we might want to handle the case where we have multiple search results on the same line, which we should optimize the DOM read operations to 1.

oh actually it seems to run extractSelectionLine all the time, can we make sure that it's also behind the experimental flag?

#167952 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all callers of this function should check shouldGetSearchPreviewInfo is True. It shouldn't be run unless it needs to.

const range = selection.getRangeAt(0);

// we need to keep a reference to the old selection range to re-apply later
const oldRange = range.cloneRange();
const captureLength = selection.toString().length;

// use selection API to modify selection to get entire line (the first line if multi-select)

// collapse selection to start so that the cursor position is at beginning of match
selection.collapseToStart();

// extend selection in both directions to select the line
selection.modify('move', 'backward', 'lineboundary');
selection.modify('extend', 'forward', 'lineboundary');

const line = selection.toString();

// using the original range and the new range, we can find the offset of the match from the line start.
const rangeStart = getStartOffset(selection.getRangeAt(0), oldRange);

// line range for match
const lineRange = {
start: rangeStart,
end: rangeStart + captureLength,
};

// re-add the old range so that the selection is restored
selection.removeAllRanges();
selection.addRange(oldRange);

return { line, range: lineRange };
}

function getStartOffset(lineRange: Range, originalRange: Range) {
// sometimes, the old and new range are in different DOM elements (ie: when the match is inside of <b></b>)
// so we need to find the first common ancestor DOM element and find the positions of the old and new range relative to that.
const firstCommonAncestor = findFirstCommonAncestor(lineRange.startContainer, originalRange.startContainer);

const selectionOffset = getSelectionOffsetRelativeTo(firstCommonAncestor, lineRange.startContainer) + lineRange.startOffset;
const textOffset = getSelectionOffsetRelativeTo(firstCommonAncestor, originalRange.startContainer) + originalRange.startOffset;
return textOffset - selectionOffset;
}

// modified from https://stackoverflow.com/a/68583466/16253823
function findFirstCommonAncestor(nodeA: Node, nodeB: Node) {
const range = new Range();
range.setStart(nodeA, 0);
range.setEnd(nodeB, 0);
return range.commonAncestorContainer;
}

// modified from https://stackoverflow.com/a/48812529/16253823
function getSelectionOffsetRelativeTo(parentElement: Node, currentNode: Node | null): number {
if (!currentNode) {
return 0;
}
let offset = 0;

if (currentNode === parentElement || !parentElement.contains(currentNode)) {
return offset;
}


// count the number of chars before the current dom elem and the start of the dom
let prevSibling = currentNode.previousSibling;
while (prevSibling) {
const nodeContent = prevSibling.nodeValue || '';
offset += nodeContent.length;
prevSibling = prevSibling.previousSibling;
}

return offset + getSelectionOffsetRelativeTo(parentElement, currentNode.parentNode);
}

const find = (query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean }) => {
let find = true;
const matches: IFindMatch[] = [];

Expand Down Expand Up @@ -1001,7 +1085,8 @@ async function webviewPreloads(ctx: PreloadContext) {
cellId: preview.id,
container: preview,
isShadow: true,
originalRange: shadowSelection.getRangeAt(0)
originalRange: shadowSelection.getRangeAt(0),
searchPreviewInfo: options.shouldGetSearchPreviewInfo ? extractSelectionLine(shadowSelection) : undefined,
});
}
}
Expand All @@ -1021,7 +1106,8 @@ async function webviewPreloads(ctx: PreloadContext) {
cellId: cellId,
container: outputNode,
isShadow: true,
originalRange: shadowSelection.getRangeAt(0)
originalRange: shadowSelection.getRangeAt(0),
searchPreviewInfo: options.shouldGetSearchPreviewInfo ? extractSelectionLine(shadowSelection) : undefined,
});
}
}
Expand All @@ -1039,7 +1125,8 @@ async function webviewPreloads(ctx: PreloadContext) {
cellId: lastEl.cellId,
container: lastEl.container,
isShadow: false,
originalRange: selection.getRangeAt(0)
originalRange: selection.getRangeAt(0),
searchPreviewInfo: options.shouldGetSearchPreviewInfo ? extractSelectionLine(selection) : undefined,
});

} else {
Expand All @@ -1059,7 +1146,8 @@ async function webviewPreloads(ctx: PreloadContext) {
cellId: cellId,
container: node,
isShadow: false,
originalRange: selection.getRangeAt(0)
originalRange: selection.getRangeAt(0),
searchPreviewInfo: options.shouldGetSearchPreviewInfo ? extractSelectionLine(selection) : undefined,
});
}
break;
Expand Down Expand Up @@ -1095,7 +1183,8 @@ async function webviewPreloads(ctx: PreloadContext) {
type: match.type,
id: match.id,
cellId: match.cellId,
index
index,
searchPreviewInfo: match.searchPreviewInfo,
}))
});
};
Expand Down
19 changes: 19 additions & 0 deletions src/vs/workbench/contrib/search/browser/media/searchview.css
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,22 @@
.monaco-workbench .search-view .monaco-list.element-focused .monaco-list-row.focused.selected:not(.highlighted) .action-label:focus {
outline-color: var(--vscode-list-activeSelectionForeground)
}

.monaco-workbench .search-container .monaco-custom-toggle.disabled {
opacity: 0.3;
cursor: default;
user-select: none;
-webkit-user-select: none;
pointer-events: none;
background-color: inherit !important;
}

.monaco-workbench .search-container .find-filter-button {
color: inherit;
margin-left: 2px;
float: left;
cursor: pointer;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
27 changes: 16 additions & 11 deletions src/vs/workbench/contrib/search/browser/replaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import * as network from 'vs/base/common/network';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, IReference } from 'vs/base/common/lifecycle';
import { IReplaceService } from 'vs/workbench/contrib/search/browser/replace';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IModelService } from 'vs/editor/common/services/model';
Expand All @@ -27,7 +27,7 @@ import { ILabelService } from 'vs/platform/label/common/label';
import { dirname } from 'vs/base/common/resources';
import { Promises } from 'vs/base/common/async';
import { SaveSourceRegistry } from 'vs/workbench/common/editor';
import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellUri, IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService';

const REPLACE_PREVIEW = 'replacePreview';
Expand Down Expand Up @@ -116,10 +116,13 @@ export class ReplaceService implements IReplaceService {
if (e.resource.scheme === network.Schemas.vscodeNotebookCell) {
const notebookResource = CellUri.parse(e.resource)?.notebook;
if (notebookResource) {
// todo: find whether there is a common API for saving notebooks and text files
const ref = await this.notebookEditorModelResolverService.resolve(notebookResource);
await ref.object.save({ source: ReplaceService.REPLACE_SAVE_SOURCE });
ref.dispose();
let ref: IReference<IResolvedNotebookEditorModel> | undefined;
try {
ref = await this.notebookEditorModelResolverService.resolve(notebookResource);
await ref.object.save({ source: ReplaceService.REPLACE_SAVE_SOURCE });
} finally {
ref?.dispose();
}
}
return;
} else {
Expand Down Expand Up @@ -196,8 +199,11 @@ export class ReplaceService implements IReplaceService {

if (arg instanceof Match) {
if (arg instanceof NotebookMatch) {
const match = <NotebookMatch>arg;
edits.push(this.createEdit(match, match.replaceString, match.cell.uri));
if (!arg.isWebviewMatch()) {
// only apply edits if it's not a webview match, since webview matches are read-only
const match = <NotebookMatch>arg;
edits.push(this.createEdit(match, match.replaceString, match.cell.uri));
}
} else {
const match = <Match>arg;
edits.push(this.createEdit(match, match.replaceString, resource));
Expand All @@ -212,13 +218,12 @@ export class ReplaceService implements IReplaceService {
arg.forEach(element => {
const fileMatch = <FileMatch>element;
if (fileMatch.count() > 0) {
edits.push(...fileMatch.matches().map(
match => this.createEdit(match, match.replaceString, (match instanceof NotebookMatch) ? match.cell.uri : resource)
edits.push(...fileMatch.matches().flatMap(
match => this.createEdits(match, resource)
));
}
});
}

return edits;
}

Expand Down
Loading