diff --git a/packages/core/src/common/preferences/preference-schema.ts b/packages/core/src/common/preferences/preference-schema.ts index 3d1e15ca0411a..b77c01a1891f9 100644 --- a/packages/core/src/common/preferences/preference-schema.ts +++ b/packages/core/src/common/preferences/preference-schema.ts @@ -75,6 +75,7 @@ export interface PreferenceSchemaProperty extends PreferenceItem { description?: string; markdownDescription?: string; scope?: 'application' | 'machine' | 'window' | 'resource' | 'language-overridable' | 'machine-overridable' | PreferenceScope; + tags?: string[]; } export interface PreferenceDataProperty extends PreferenceItem { diff --git a/packages/notebook/src/browser/contributions/notebook-preferences.ts b/packages/notebook/src/browser/contributions/notebook-preferences.ts index 9fd02a08dc653..bb947fb756dc3 100644 --- a/packages/notebook/src/browser/contributions/notebook-preferences.ts +++ b/packages/notebook/src/browser/contributions/notebook-preferences.ts @@ -13,19 +13,71 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import { nls } from '@theia/core'; import { PreferenceSchema } from '@theia/core/lib/browser'; -export const NOTEBOOK_LINE_NUMBERS = 'notebook.lineNumbers'; +export namespace NotebookPreferences { + export const NOTEBOOK_LINE_NUMBERS = 'notebook.lineNumbers'; + export const OUTPUT_LINE_HEIGHT = 'notebook.output.lineHeight'; + export const OUTPUT_FONT_SIZE = 'notebook.output.fontSize'; + export const OUTPUT_FONT_FAMILY = 'notebook.output.fontFamily'; + export const OUTPUT_SCROLLING = 'notebook.output.scrolling'; + export const OUTPUT_WORD_WRAP = 'notebook.output.wordWrap'; + export const OUTPUT_LINE_LIMIT = 'notebook.output.textLineLimit'; +} export const notebookPreferenceSchema: PreferenceSchema = { properties: { - [NOTEBOOK_LINE_NUMBERS]: { + [NotebookPreferences.NOTEBOOK_LINE_NUMBERS]: { type: 'string', enum: ['on', 'off'], default: 'off', description: nls.localizeByDefault('Controls the display of line numbers in the cell editor.') }, + [NotebookPreferences.OUTPUT_LINE_HEIGHT]: { + // eslint-disable-next-line max-len + markdownDescription: nls.localizeByDefault('Line height of the output text within notebook cells.\n - When set to 0, editor line height is used.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values.'), + type: 'number', + default: 0, + tags: ['notebookLayout', 'notebookOutputLayout'] + }, + [NotebookPreferences.OUTPUT_FONT_SIZE]: { + markdownDescription: nls.localizeByDefault('Font size for the output text within notebook cells. When set to 0, {0} is used.', '`#editor.fontSize#`'), + type: 'number', + default: 0, + tags: ['notebookLayout', 'notebookOutputLayout'] + }, + [NotebookPreferences.OUTPUT_FONT_FAMILY]: { + markdownDescription: nls.localizeByDefault('The font family of the output text within notebook cells. When set to empty, the {0} is used.', '`#editor.fontFamily#`'), + type: 'string', + tags: ['notebookLayout', 'notebookOutputLayout'] + }, + [NotebookPreferences.OUTPUT_SCROLLING]: { + markdownDescription: nls.localizeByDefault('Initially render notebook outputs in a scrollable region when longer than the limit.'), + type: 'boolean', + tags: ['notebookLayout', 'notebookOutputLayout'], + default: false + }, + [NotebookPreferences.OUTPUT_WORD_WRAP]: { + markdownDescription: nls.localizeByDefault('Controls whether the lines in output should wrap.'), + type: 'boolean', + tags: ['notebookLayout', 'notebookOutputLayout'], + default: false + }, + [NotebookPreferences.OUTPUT_LINE_LIMIT]: { + markdownDescription: nls.localizeByDefault( + 'Controls how many lines of text are displayed in a text output. If {0} is enabled, this setting is used to determine the scroll height of the output.', + '`#notebook.output.scrolling#`'), + type: 'number', + default: 30, + tags: ['notebookLayout', 'notebookOutputLayout'], + minimum: 1, + }, + } }; diff --git a/packages/notebook/src/browser/notebook-frontend-module.ts b/packages/notebook/src/browser/notebook-frontend-module.ts index c79f729c4e143..681780f512655 100644 --- a/packages/notebook/src/browser/notebook-frontend-module.ts +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -45,6 +45,7 @@ import { NotebookLabelProviderContribution } from './contributions/notebook-labe import { NotebookOutputActionContribution } from './contributions/notebook-output-action-contribution'; import { NotebookClipboardService } from './service/notebook-clipboard-service'; import { notebookPreferenceSchema } from './contributions/notebook-preferences'; +import { NotebookOptionsService } from './service/notebook-options'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(NotebookColorContribution).toSelf().inSingletonScope(); @@ -106,4 +107,5 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(LabelProviderContribution).toService(NotebookLabelProviderContribution); bind(PreferenceContribution).toConstantValue({ schema: notebookPreferenceSchema }); + bind(NotebookOptionsService).toSelf().inSingletonScope(); }); diff --git a/packages/notebook/src/browser/service/notebook-options.ts b/packages/notebook/src/browser/service/notebook-options.ts new file mode 100644 index 0000000000000..c642e97821e3e --- /dev/null +++ b/packages/notebook/src/browser/service/notebook-options.ts @@ -0,0 +1,154 @@ + +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { PreferenceService } from '@theia/core/lib/browser'; +import { Emitter } from '@theia/core'; +import { NotebookPreferences, notebookPreferenceSchema } from '../contributions/notebook-preferences'; +import { EditorPreferences } from '@theia/editor/lib/browser'; +import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo'; +import { PixelRatio } from '@theia/monaco-editor-core/esm/vs/base/browser/browser'; + +const notebookOutputOptionsRelevantPreferences = [ + 'editor.fontSize', + 'editor.fontFamily', + NotebookPreferences.NOTEBOOK_LINE_NUMBERS, + NotebookPreferences.OUTPUT_LINE_HEIGHT, + NotebookPreferences.OUTPUT_FONT_SIZE, + NotebookPreferences.OUTPUT_FONT_FAMILY, + NotebookPreferences.OUTPUT_SCROLLING, + NotebookPreferences.OUTPUT_WORD_WRAP, + NotebookPreferences.OUTPUT_LINE_LIMIT +]; + +export interface NotebookOutputOptions { + // readonly outputNodePadding: number; + // readonly outputNodeLeftPadding: number; + // readonly previewNodePadding: number; + // readonly markdownLeftMargin: number; + // readonly leftMargin: number; + // readonly rightMargin: number; + // readonly runGutter: number; + // readonly dragAndDropEnabled: boolean; + readonly fontSize: number; + readonly outputFontSize?: number; + readonly fontFamily: string; + readonly outputFontFamily?: string; + // readonly markupFontSize: number; + // readonly markdownLineHeight: number; + readonly outputLineHeight: number; + readonly outputScrolling: boolean; + readonly outputWordWrap: boolean; + readonly outputLineLimit: number; + // readonly outputLinkifyFilePaths: boolean; + // readonly minimalError: boolean; + +} + +@injectable() +export class NotebookOptionsService { + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + @inject(EditorPreferences) + protected readonly editorPreferences: EditorPreferences; + + protected outputOptionsChangedEmitter = new Emitter(); + onDidChangeOutputOptions = this.outputOptionsChangedEmitter.event; + + protected fontInfo?: BareFontInfo; + get editorFontInfo(): BareFontInfo { + return this.getOrCreateMonacoFontInfo(); + } + + @postConstruct() + protected init(): void { + this.preferenceService.onPreferencesChanged(async preferenceChanges => { + if (notebookOutputOptionsRelevantPreferences.some(p => p in preferenceChanges)) { + this.outputOptionsChangedEmitter.fire(this.computeOutputOptions()); + } + }); + } + + computeOutputOptions(): NotebookOutputOptions { + const outputLineHeight = this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_LINE_HEIGHT); + + const fontSize = this.preferenceService.get('editor.fontSize')!; + const outputFontSize = this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_FONT_SIZE); + + return { + fontSize, + outputFontSize: outputFontSize, + fontFamily: this.preferenceService.get('editor.fontFamily')!, + outputFontFamily: this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_FONT_FAMILY), + outputLineHeight: this.computeOutputLineHeight(outputLineHeight, outputFontSize ?? fontSize), + outputScrolling: this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_SCROLLING)!, + outputWordWrap: this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_WORD_WRAP)!, + outputLineLimit: this.getNotebookPreferenceWithDefault(NotebookPreferences.OUTPUT_LINE_LIMIT)! + }; + } + + protected getNotebookPreferenceWithDefault(key: string): T { + return this.preferenceService.get(key, notebookPreferenceSchema.properties?.[key]?.default as T); + } + + protected computeOutputLineHeight(lineHeight: number, outputFontSize: number): number { + const minimumLineHeight = 9; + + if (lineHeight === 0) { + // use editor line height + lineHeight = this.editorFontInfo.lineHeight; + } else if (lineHeight < minimumLineHeight) { + // Values too small to be line heights in pixels are in ems. + let fontSize = outputFontSize; + if (fontSize === 0) { + fontSize = this.preferenceService.get('editor.fontSize')!; + } + + lineHeight = lineHeight * fontSize; + } + + // Enforce integer, minimum constraints + lineHeight = Math.round(lineHeight); + if (lineHeight < minimumLineHeight) { + lineHeight = minimumLineHeight; + } + + return lineHeight; + } + + protected getOrCreateMonacoFontInfo(): BareFontInfo { + if (!this.fontInfo) { + this.fontInfo = this.createFontInfo(); + this.editorPreferences.onPreferenceChanged(e => this.fontInfo = this.createFontInfo()); + } + return this.fontInfo; + } + + protected createFontInfo(): BareFontInfo { + return BareFontInfo.createFromRawSettings({ + fontFamily: this.editorPreferences['editor.fontFamily'], + fontWeight: String(this.editorPreferences['editor.fontWeight']), + fontSize: this.editorPreferences['editor.fontSize'], + fontLigatures: this.editorPreferences['editor.fontLigatures'], + lineHeight: this.editorPreferences['editor.lineHeight'], + letterSpacing: this.editorPreferences['editor.letterSpacing'], + }, PixelRatio.value); + } + +} diff --git a/packages/notebook/src/browser/view-model/notebook-cell-model.ts b/packages/notebook/src/browser/view-model/notebook-cell-model.ts index 038666cd24d21..df4b2cf96c2cb 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -30,7 +30,7 @@ import { NotebookCellOutputsSplice } from '../notebook-types'; import { NotebookMonacoTextModelService } from '../service/notebook-monaco-text-model-service'; import { NotebookCellOutputModel } from './notebook-cell-output-model'; import { PreferenceService } from '@theia/core/lib/browser'; -import { NOTEBOOK_LINE_NUMBERS } from '../contributions/notebook-preferences'; +import { NotebookPreferences } from '../contributions/notebook-preferences'; import { LanguageService } from '@theia/core/lib/browser/language-service'; export const NotebookCellModelFactory = Symbol('NotebookModelFactory'); @@ -245,13 +245,13 @@ export class NotebookCellModel implements NotebookCell, Disposable { this._internalMetadata = this.props.internalMetadata ?? {}; this.editorOptions = { - lineNumbers: this.preferenceService.get(NOTEBOOK_LINE_NUMBERS) + lineNumbers: this.preferenceService.get(NotebookPreferences.NOTEBOOK_LINE_NUMBERS) }; this.toDispose.push(this.preferenceService.onPreferenceChanged(e => { - if (e.preferenceName === NOTEBOOK_LINE_NUMBERS) { + if (e.preferenceName === NotebookPreferences.NOTEBOOK_LINE_NUMBERS) { this.editorOptions = { ...this.editorOptions, - lineNumbers: this.preferenceService.get(NOTEBOOK_LINE_NUMBERS) + lineNumbers: this.preferenceService.get(NotebookPreferences.NOTEBOOK_LINE_NUMBERS) }; } })); diff --git a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx index 596cc22af4c26..9e5fc25129973 100644 --- a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx @@ -32,8 +32,7 @@ import { CommandRegistry, DisposableCollection, nls } from '@theia/core'; import { NotebookContextManager } from '../service/notebook-context-manager'; import { NotebookViewportService } from './notebook-viewport-service'; import { EditorPreferences } from '@theia/editor/lib/browser'; -import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo'; -import { PixelRatio } from '@theia/monaco-editor-core/esm/vs/base/browser/browser'; +import { NotebookOptionsService } from '../service/notebook-options'; @injectable() export class NotebookCodeCellRenderer implements CellRenderer { @@ -64,7 +63,8 @@ export class NotebookCodeCellRenderer implements CellRenderer { @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; - protected fontInfo: BareFontInfo | undefined; + @inject(NotebookOptionsService) + protected readonly notebookOptionsService: NotebookOptionsService; render(notebookModel: NotebookModel, cell: NotebookCellModel, handle: number): React.ReactNode { return
@@ -81,7 +81,7 @@ export class NotebookCodeCellRenderer implements CellRenderer { monacoServices={this.monacoServices} notebookContextManager={this.notebookContextManager} notebookViewportService={this.notebookViewportService} - fontInfo={this.getOrCreateMonacoFontInfo()} /> + fontInfo={this.notebookOptionsService.editorFontInfo} /> this.fontInfo = this.createFontInfo()); - } - return this.fontInfo; - } - - protected createFontInfo(): BareFontInfo { - return BareFontInfo.createFromRawSettings({ - fontFamily: this.editorPreferences['editor.fontFamily'], - fontWeight: String(this.editorPreferences['editor.fontWeight']), - fontSize: this.editorPreferences['editor.fontSize'], - fontLigatures: this.editorPreferences['editor.fontLigatures'], - lineHeight: this.editorPreferences['editor.lineHeight'], - letterSpacing: this.editorPreferences['editor.letterSpacing'], - }, PixelRatio.value); - } } export interface NotebookCodeCellStatusProps { diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx index 98cac69ef734c..c2f7555e7bbf3 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx @@ -35,6 +35,7 @@ import { CellUri } from '@theia/notebook/lib/common'; import { Disposable, DisposableCollection, nls, QuickPickService } from '@theia/core'; import { NotebookCellOutputModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-output-model'; import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; +import { NotebookOptionsService, NotebookOutputOptions } from '@theia/notebook/lib/browser/service/notebook-options'; const CellModel = Symbol('CellModel'); const Notebook = Symbol('NotebookModel'); @@ -147,6 +148,11 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { @inject(AdditionalNotebookCellOutputCss) protected readonly additionalOutputCss: string; + @inject(NotebookOptionsService) + protected readonly notebookOptionsService: NotebookOptionsService; + + protected options: NotebookOutputOptions; + readonly id = generateUuid(); protected editor: NotebookEditorWidget | undefined; @@ -161,6 +167,11 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { @postConstruct() protected async init(): Promise { this.editor = this.notebookEditorWidgetService.getNotebookEditor(NOTEBOOK_EDITOR_ID_PREFIX + CellUri.parse(this.cell.uri)?.notebook); + this.options = this.notebookOptionsService.computeOutputOptions(); + this.toDispose.push(this.notebookOptionsService.onDidChangeOutputOptions(options => { + this.options = options; + this.updateStyles(); + })); this.toDispose.push(this.cell.onDidChangeOutputs(outputChange => this.updateOutput(outputChange))); this.toDispose.push(this.cell.onDidChangeOutputItems(output => { @@ -272,6 +283,7 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { switch (message.type) { case 'initialized': this.updateOutput({ newOutputs: this.cell.outputs, start: 0, deleteCount: 0 }); + this.updateStyles(); break; case 'customRendererMessage': // console.log('from webview customRendererMessage ', message.rendererId, '', JSON.stringify(message.message)); @@ -302,6 +314,22 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { return kernelPreloads.concat(staticPreloads); } + protected updateStyles(): void { + this.webviewWidget.sendMessage({ + type: 'notebookStyles', + styles: this.generateStyles() + }); + } + + protected generateStyles(): { [key: string]: string } { + return { + 'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`, + 'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`, + 'notebook-cell-output-max-height': `${this.options.outputLineHeight * this.options.outputLineLimit}px`, + 'notebook-cell-output-font-family': this.options.outputFontFamily || this.options.fontFamily, + }; + } + private async createWebviewContent(): Promise { const isWorkspaceTrusted = await this.workspaceTrustService.getWorkspaceTrust(); const preloads = this.preloadsScriptString(isWorkspaceTrusted); @@ -325,10 +353,10 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { const ctx: PreloadContext = { isWorkspaceTrusted, rendererData: this.notebookRendererRegistry.notebookRenderers, - renderOptions: { // TODO these should be changeable in the settings - lineLimit: 30, - outputScrolling: false, - outputWordWrap: false, + renderOptions: { + lineLimit: this.options.outputLineLimit, + outputScrolling: this.options.outputScrolling, + outputWordWrap: this.options.outputWordWrap, }, staticPreloadsData: this.getPreloads() }; diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts b/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts index 8818378ed4446..7e01a55751954 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts @@ -569,6 +569,24 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { } break; } + case 'notebookStyles': { + const documentStyle = window.document.documentElement.style; + + for (let i = documentStyle.length - 1; i >= 0; i--) { + const property = documentStyle[i]; + + // Don't remove properties that the webview might have added separately + if (property && property.startsWith('--notebook-')) { + documentStyle.removeProperty(property); + } + } + + // Re-add new properties + for (const [name, value] of Object.entries(event.data.styles)) { + documentStyle.setProperty(`--${name}`, value); + } + break; + } } }); window.addEventListener('wheel', handleWheel); diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts b/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts index 4989fbef9df2e..442c7baf86ef1 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts @@ -59,7 +59,18 @@ export interface PreloadMessage { readonly resources: string[]; } -export type ToWebviewMessage = UpdateRenderersMessage | OutputChangedMessage | ChangePreferredMimetypeMessage | CustomRendererMessage | KernelMessage | PreloadMessage; +export interface notebookStylesMessage { + readonly type: 'notebookStyles'; + styles: Record; +} + +export type ToWebviewMessage = UpdateRenderersMessage + | OutputChangedMessage + | ChangePreferredMimetypeMessage + | CustomRendererMessage + | KernelMessage + | PreloadMessage + | notebookStylesMessage; export interface WebviewInitialized { readonly type: 'initialized'; diff --git a/packages/preferences/src/browser/preference-tree-model.ts b/packages/preferences/src/browser/preference-tree-model.ts index c03ecf19afb96..689e186641fba 100644 --- a/packages/preferences/src/browser/preference-tree-model.ts +++ b/packages/preferences/src/browser/preference-tree-model.ts @@ -67,6 +67,7 @@ export class PreferenceTreeModel extends TreeModelImpl { protected lastSearchedFuzzy: string = ''; protected lastSearchedLiteral: string = ''; + protected lastSearchedTags: string[] = []; protected _currentScope: number = Number(Preference.DEFAULT_SCOPE.scope); protected _isFiltered: boolean = false; protected _currentRows: Map = new Map(); @@ -110,8 +111,10 @@ export class PreferenceTreeModel extends TreeModelImpl { this.updateFilteredRows(PreferenceFilterChangeSource.Scope); }), this.filterInput.onFilterChanged(newSearchTerm => { - this.lastSearchedLiteral = newSearchTerm; - this.lastSearchedFuzzy = newSearchTerm.replace(/\s/g, ''); + this.lastSearchedTags = Array.from(newSearchTerm.matchAll(/@tag:([^\s]+)/g)).map(match => match[0].slice(5)); + const newSearchTermWithoutTags = newSearchTerm.replace(/@tag:[^\s]+/g, ''); + this.lastSearchedLiteral = newSearchTermWithoutTags; + this.lastSearchedFuzzy = newSearchTermWithoutTags.replace(/\s/g, ''); this._isFiltered = newSearchTerm.length > 2; if (this.isFiltered) { this.expandAll(); @@ -183,6 +186,9 @@ export class PreferenceTreeModel extends TreeModelImpl { if (node.id.startsWith(COMMONLY_USED_SECTION_PREFIX)) { return false; } + if (!this.lastSearchedTags.every(tag => node.preference.data.tags?.includes(tag))) { + return false; + } return fuzzy.test(this.lastSearchedFuzzy, prefID) // search matches preference name. // search matches description. Fuzzy isn't ideal here because the score depends on the order of discovery. || (node.preference.data.description ?? '').includes(this.lastSearchedLiteral);