mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 12:51:55 +00:00
svelte: Remove full page scrolling of repo pages (#62024)
This commit removes the full page scrolling behavior for repo root, directory and file pages. The implementation had several issues. In the new implementation the scroll container is CodeMirror itself. It provides the following behavior: - Show the top of the file when navigating to a new file. - Scroll the selected line into view when opening a URL containing a position. - Do not scroll when selecting a line (currently broken). - Restore previous scroll position when navigating backward or forward in the browser history. - Toggling the blame view maintains the current line view (well, almost but that's as best as we can do at the moment). Additional changes in this commit: - Removed logic to enable/disable full page scrolling. - Remove CSS for making elements `sticky`. - Update "scroll selected line into view" logic to prevent scrolling when restoring the previous scroll position. - Update `codemirror/view` to include a recent fix for a scroll related bug.
This commit is contained in:
parent
a2b170dc9e
commit
ff1ea373c8
@ -28,8 +28,6 @@
|
||||
languages: string[]
|
||||
}
|
||||
|
||||
const extensionsCompartment = new Compartment()
|
||||
|
||||
const defaultTheme = EditorView.theme({
|
||||
'&': {
|
||||
height: '100%',
|
||||
@ -42,6 +40,7 @@
|
||||
lineHeight: '1rem',
|
||||
fontFamily: 'var(--code-font-family)',
|
||||
fontSize: 'var(--code-font-size)',
|
||||
overflow: 'auto',
|
||||
},
|
||||
'.cm-content': {
|
||||
padding: 0,
|
||||
@ -106,20 +105,12 @@
|
||||
defaultTheme,
|
||||
linkify,
|
||||
]
|
||||
|
||||
function configureSyntaxHighlighting(content: string, lsif: string): Extension {
|
||||
return lsif ? syntaxHighlight.of({ content, lsif }) : []
|
||||
}
|
||||
|
||||
function configureMiscSettings({ wrapLines }: { wrapLines: boolean }): Extension {
|
||||
return [wrapLines ? EditorView.lineWrapping : []]
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import '$lib/highlight.scss'
|
||||
|
||||
import { Compartment, EditorState, type Extension } from '@codemirror/state'
|
||||
import { EditorState, type Extension } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
@ -136,14 +127,22 @@
|
||||
linkify,
|
||||
createCodeIntelExtension,
|
||||
syncSelection,
|
||||
temporaryTooltip,
|
||||
showBlame as showBlameColumn,
|
||||
blameData as blameDataFacet,
|
||||
type BlameHunkData,
|
||||
lockFirstVisibleLine,
|
||||
temporaryTooltip,
|
||||
} from '$lib/web'
|
||||
|
||||
import BlameDecoration from './blame/BlameDecoration.svelte'
|
||||
import { type Range, staticHighlights } from './codemirror/static-highlights'
|
||||
import {
|
||||
createCompartments,
|
||||
restoreScrollSnapshot,
|
||||
type ExtensionType,
|
||||
type ScrollSnapshot,
|
||||
getScrollSnapshot as getScrollSnapshot_internal,
|
||||
} from './codemirror/utils'
|
||||
import { goToDefinition, openImplementations, openReferences } from './repo/blob'
|
||||
|
||||
export let blobInfo: BlobInfo
|
||||
@ -152,22 +151,35 @@
|
||||
export let selectedLines: LineOrPositionOrRange | null = null
|
||||
export let codeIntelAPI: CodeIntelAPI
|
||||
export let staticHighlightRanges: Range[] = []
|
||||
/**
|
||||
* The initial scroll position when the editor is first mounted.
|
||||
* Changing the value afterwards has no effect.
|
||||
*/
|
||||
export let initialScrollPosition: ScrollSnapshot | null = null
|
||||
|
||||
export let showBlame: boolean = false
|
||||
export let blameData: BlameHunkData | undefined = undefined
|
||||
|
||||
export function getScrollSnapshot(): ScrollSnapshot | null {
|
||||
return view ? getScrollSnapshot_internal(view) : null
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ selectline: SelectedLineRange }>()
|
||||
|
||||
let editor: EditorView
|
||||
let container: HTMLDivElement | null = null
|
||||
|
||||
const lineNumbers = selectableLineNumbers({
|
||||
onSelection(range) {
|
||||
dispatch('selectline', range)
|
||||
},
|
||||
initialSelection: selectedLines?.line === undefined ? null : selectedLines,
|
||||
const extensionsCompartment = createCompartments({
|
||||
selectableLineNumbers: null,
|
||||
syntaxHighlighting: null,
|
||||
lineWrapping: null,
|
||||
temporaryTooltip,
|
||||
codeIntelExtension: null,
|
||||
staticExtensions,
|
||||
staticHighlightExtension: null,
|
||||
blameDataExtension: null,
|
||||
blameColumnExtension: null,
|
||||
})
|
||||
|
||||
let container: HTMLDivElement | null = null
|
||||
let view: EditorView | undefined = undefined
|
||||
|
||||
$: documentInfo = {
|
||||
repoName: blobInfo.repoName,
|
||||
commitID: blobInfo.commitID,
|
||||
@ -198,8 +210,8 @@
|
||||
}
|
||||
},
|
||||
})
|
||||
$: settings = configureMiscSettings({ wrapLines })
|
||||
$: sh = configureSyntaxHighlighting(blobInfo.content, highlights)
|
||||
$: lineWrapping = wrapLines ? EditorView.lineWrapping : []
|
||||
$: syntaxHighlighting = highlights ? syntaxHighlight.of({ content: blobInfo.content, lsif: highlights }) : []
|
||||
$: staticHighlightExtension = staticHighlights(staticHighlightRanges)
|
||||
|
||||
$: blameColumnExtension = showBlame
|
||||
@ -216,51 +228,96 @@
|
||||
: []
|
||||
$: blameDataExtension = blameDataFacet(blameData)
|
||||
|
||||
$: extensions = [
|
||||
sh,
|
||||
settings,
|
||||
lineNumbers,
|
||||
temporaryTooltip,
|
||||
codeIntelExtension,
|
||||
staticExtensions,
|
||||
staticHighlightExtension,
|
||||
blameColumnExtension,
|
||||
blameDataExtension,
|
||||
]
|
||||
|
||||
function update(blobInfo: BlobInfo, extensions: Extension, range: LineOrPositionOrRange | null) {
|
||||
if (editor) {
|
||||
// TODO(fkling): Find a way to combine this into a single transaction.
|
||||
if (editor.state.sliceDoc() !== blobInfo.content) {
|
||||
editor.setState(
|
||||
EditorState.create({ doc: blobInfo.content, extensions: extensionsCompartment.of(extensions) })
|
||||
)
|
||||
} else {
|
||||
editor.dispatch({ effects: [extensionsCompartment.reconfigure(extensions)] })
|
||||
}
|
||||
editor.dispatch({
|
||||
effects: setSelectedLines.of(range?.line && isValidLineRange(range, editor.state.doc) ? range : null),
|
||||
})
|
||||
|
||||
if (range) {
|
||||
syncSelection(editor, range)
|
||||
}
|
||||
// Reinitialize the editor when its content changes. Update only the extensions when they change.
|
||||
$: update(view => {
|
||||
// blameColumnExtension is omitted here. It's updated separately below because we need to
|
||||
// apply additional effects when it changes (but only when it changes).
|
||||
const extensions: Partial<ExtensionType<typeof extensionsCompartment>> = {
|
||||
codeIntelExtension,
|
||||
lineWrapping,
|
||||
syntaxHighlighting,
|
||||
staticHighlightExtension,
|
||||
blameDataExtension,
|
||||
}
|
||||
}
|
||||
if (view.state.sliceDoc() !== blobInfo.content) {
|
||||
view.setState(createEditorState(blobInfo, extensions))
|
||||
} else {
|
||||
extensionsCompartment.update(view, extensions)
|
||||
}
|
||||
})
|
||||
|
||||
$: update(blobInfo, extensions, selectedLines)
|
||||
// Show/hide the blame column and ensure that the style changes do not change the scroll position
|
||||
$: update(view => {
|
||||
extensionsCompartment.update(view, { blameColumnExtension }, ...lockFirstVisibleLine(view))
|
||||
})
|
||||
|
||||
// Update the selected lines. This will scroll the selected lines into view. Also set the editor's
|
||||
// selection (essentially the cursor position) to the selected lines. This is necessary in case the
|
||||
// selected range references a symbol.
|
||||
$: update(view => {
|
||||
view.dispatch({
|
||||
effects: setSelectedLines.of(
|
||||
selectedLines?.line && isValidLineRange(selectedLines, view.state.doc) ? selectedLines : null
|
||||
),
|
||||
})
|
||||
if (selectedLines) {
|
||||
syncSelection(view, selectedLines)
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (container) {
|
||||
editor = new EditorView({
|
||||
state: EditorState.create({ doc: blobInfo.content, extensions: extensionsCompartment.of(extensions) }),
|
||||
view = new EditorView({
|
||||
// On first render initialize all extensions
|
||||
state: createEditorState(blobInfo, {
|
||||
codeIntelExtension,
|
||||
lineWrapping,
|
||||
syntaxHighlighting,
|
||||
staticHighlightExtension,
|
||||
blameDataExtension,
|
||||
blameColumnExtension,
|
||||
}),
|
||||
parent: container,
|
||||
})
|
||||
if (selectedLines) {
|
||||
syncSelection(editor, selectedLines)
|
||||
syncSelection(view, selectedLines)
|
||||
}
|
||||
if (initialScrollPosition) {
|
||||
restoreScrollSnapshot(view, initialScrollPosition)
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
view?.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
// Helper function to update the editor state whithout depending on the view variable
|
||||
// (those updates should only run on subsequent updates)
|
||||
function update(updater: (view: EditorView) => void) {
|
||||
if (view) {
|
||||
updater(view)
|
||||
}
|
||||
}
|
||||
|
||||
function createEditorState(blobInfo: BlobInfo, extensions: Partial<ExtensionType<typeof extensionsCompartment>>) {
|
||||
return EditorState.create({
|
||||
doc: blobInfo.content,
|
||||
extensions: extensionsCompartment.init({
|
||||
selectableLineNumbers: selectableLineNumbers({
|
||||
onSelection(range) {
|
||||
dispatch('selectline', range)
|
||||
},
|
||||
initialSelection: selectedLines?.line === undefined ? null : selectedLines,
|
||||
// We don't want to scroll the selected line into view when a scroll position is explicitly set.
|
||||
skipInitialScrollIntoView: initialScrollPosition !== null,
|
||||
}),
|
||||
...extensions,
|
||||
}),
|
||||
selection: {
|
||||
anchor: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if browser}
|
||||
@ -273,9 +330,10 @@
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: contents;
|
||||
--blame-decoration-width: 400px;
|
||||
--blame-recency-width: 4px;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
pre {
|
||||
margin: 0;
|
||||
|
||||
@ -1,59 +1,122 @@
|
||||
import { Compartment, type StateEffect, type Extension } from '@codemirror/state'
|
||||
import type { EditorView } from '@codemirror/view'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
|
||||
type Extensions = Record<string, Extension>
|
||||
type UpdatedExtensions<T extends Extensions> = { [key in keyof T]: Extension }
|
||||
type Extensions = Record<string, Extension | null>
|
||||
type UpdatedExtensions<T extends Extensions> = { [key in keyof T]?: Extension | null }
|
||||
export type ExtensionType<T> = T extends Compartments<infer U> ? UpdatedExtensions<U> : never
|
||||
|
||||
interface Compartments<T extends Extensions> {
|
||||
extension: Extension
|
||||
/**
|
||||
* Initialize compartments with a different value
|
||||
* Initialize compartments with a different value.
|
||||
*
|
||||
* @param extensions The values for the compartments
|
||||
* @returns An extension
|
||||
*/
|
||||
init(extensions: UpdatedExtensions<T>): Extension
|
||||
/**
|
||||
* Update compartments. Only compartments for which the provided
|
||||
* value has changed will be updated.
|
||||
* Update compartments. Only compartments for which the provided value has changed will be updated.
|
||||
* Additional effects can be provided to be dispatched with the compartment updates, but they will
|
||||
* only be dispatched if at least one compartment has changed.
|
||||
*
|
||||
* @param view The editor view
|
||||
* @param extensions The updated values for the compartments
|
||||
* @param additionalEffects Additional effects to be dispatched with the compartment updates
|
||||
*/
|
||||
update(view: EditorView, extensions: UpdatedExtensions<T>): void
|
||||
update(view: EditorView, extensions: UpdatedExtensions<T>, ...additionalEffects: StateEffect<unknown>[]): void
|
||||
}
|
||||
|
||||
const emptyExtension: Extension = []
|
||||
|
||||
/**
|
||||
* Helper function for creating a compartments extension. Each record
|
||||
* entry will get its own compartment and the value will be the initial
|
||||
* value of the compartment.
|
||||
* The order/presedence of the extensions is determined by the order of the
|
||||
* keys in the initialExtensions record.
|
||||
*
|
||||
* @param initialExtensions Initial values for the compartments
|
||||
* @returns Compartments extension
|
||||
*/
|
||||
export function createCompartments<T extends Record<string, Extension>>(extensions: T): Compartments<T> {
|
||||
const compartments: Record<string, Compartment> = {}
|
||||
export function createCompartments<T extends Extensions>(initialExtensions: T): Compartments<T> {
|
||||
const compartments: Map<string, Compartment> = new Map()
|
||||
|
||||
function init(extensions: UpdatedExtensions<T>, compartments: Map<string, Compartment>): Extension {
|
||||
const values: Map<string, Extension> = new Map()
|
||||
|
||||
function init(extensions: UpdatedExtensions<T>, compartments: Record<string, Compartment>): Extension {
|
||||
const extension: Extension[] = []
|
||||
for (const [name, ext] of Object.entries(extensions)) {
|
||||
let compartment = compartments[name]
|
||||
let compartment = compartments.get(name)
|
||||
if (!compartment) {
|
||||
compartment = compartments[name] = new Compartment()
|
||||
compartments.set(name, (compartment = new Compartment()))
|
||||
}
|
||||
extension.push(compartment.of(ext))
|
||||
values.set(name, compartment.of(ext ?? emptyExtension))
|
||||
}
|
||||
return extension
|
||||
|
||||
// Return extensions in the order of the initialExtensions record
|
||||
return Array.from(compartments.keys(), name => values.get(name) ?? emptyExtension)
|
||||
}
|
||||
|
||||
return {
|
||||
extension: init(extensions, compartments),
|
||||
extension: init(initialExtensions, compartments),
|
||||
init(extensions) {
|
||||
return init(extensions, compartments)
|
||||
return init({ ...initialExtensions, ...extensions }, compartments)
|
||||
},
|
||||
update(view, extensions) {
|
||||
update(view, extensions, ...additionalEffects) {
|
||||
const effects: StateEffect<unknown>[] = []
|
||||
|
||||
for (const [name, ext] of Object.entries(extensions)) {
|
||||
if (compartments[name].get(view.state) !== ext) {
|
||||
effects.push(compartments[name].reconfigure(ext))
|
||||
const compartment = compartments.get(name)
|
||||
if (compartment && compartment.get(view.state) !== ext) {
|
||||
effects.push(compartment.reconfigure(ext ?? emptyExtension))
|
||||
}
|
||||
}
|
||||
|
||||
if (effects.length > 0) {
|
||||
view.dispatch({ effects })
|
||||
view.dispatch({ effects: effects.concat(additionalEffects) })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An object representing the scroll state of a CodeMirror instance.
|
||||
*/
|
||||
export interface ScrollSnapshot {
|
||||
scrollTop?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the editors scroll state that is serializable (unlike CodeMirror's
|
||||
* native scroll snapshot).
|
||||
*
|
||||
* @param view The editor view
|
||||
* @returns The scroll snapshot
|
||||
*/
|
||||
export function getScrollSnapshot(view: EditorView): ScrollSnapshot {
|
||||
return {
|
||||
scrollTop: view.scrollDOM.scrollTop,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the scroll state of the editor from a snapshot.
|
||||
*
|
||||
* @param view The editor view
|
||||
* @param snapshot The scroll snapshot
|
||||
*/
|
||||
export function restoreScrollSnapshot(view: EditorView, snapshot: ScrollSnapshot): void {
|
||||
const { scrollTop } = snapshot
|
||||
|
||||
if (scrollTop !== undefined) {
|
||||
// We are using request measure here to ensure that the DOM has been updated
|
||||
// before updating the scroll position.
|
||||
view.requestMeasure({
|
||||
read() {
|
||||
return null
|
||||
},
|
||||
write(_measure, view) {
|
||||
view.scrollDOM.scrollTop = scrollTop
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,8 +74,6 @@
|
||||
align-items: baseline;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
background-color: var(--color-bg-1);
|
||||
z-index: 1;
|
||||
gap: 0.5rem;
|
||||
|
||||
@ -81,5 +81,3 @@ export function createLocalWritable<T>(localStorageKey: string, defaultValue: T)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const scrollAll = writable(false)
|
||||
|
||||
@ -10,6 +10,7 @@ export { linkify } from '@sourcegraph/web/src/repo/blob/codemirror/links'
|
||||
export { createCodeIntelExtension } from '@sourcegraph/web/src/repo/blob/codemirror/codeintel/extension'
|
||||
export type { TooltipViewOptions } from '@sourcegraph/web/src/repo/blob/codemirror/codeintel/api'
|
||||
export { positionToOffset, locationToURL } from '@sourcegraph/web/src/repo/blob/codemirror/utils'
|
||||
export { lockFirstVisibleLine } from '@sourcegraph/web/src/repo/blob/codemirror/lock-line'
|
||||
export { syncSelection } from '@sourcegraph/web/src/repo/blob/codemirror/codeintel/token-selection'
|
||||
export {
|
||||
showTemporaryTooltip,
|
||||
|
||||
@ -3,9 +3,8 @@
|
||||
|
||||
import { browser } from '$app/environment'
|
||||
import { isErrorLike } from '$lib/common'
|
||||
import { classNames } from '$lib/dom'
|
||||
import { TemporarySettingsStorage } from '$lib/shared'
|
||||
import { isLightTheme, setAppContext, scrollAll } from '$lib/stores'
|
||||
import { isLightTheme, setAppContext } from '$lib/stores'
|
||||
import { createTemporarySettingsStorage } from '$lib/temporarySettings'
|
||||
|
||||
import Header from './Header.svelte'
|
||||
@ -93,8 +92,6 @@
|
||||
<meta name="description" content="Code search" />
|
||||
</svelte:head>
|
||||
|
||||
<svelte:body use:classNames={$scrollAll ? '' : 'overflowHidden'} />
|
||||
|
||||
{#await data.globalSiteAlerts then globalSiteAlerts}
|
||||
{#if globalSiteAlerts}
|
||||
<GlobalNotification globalAlerts={globalSiteAlerts} />
|
||||
@ -108,15 +105,11 @@
|
||||
</main>
|
||||
|
||||
<style lang="scss">
|
||||
:global(body.overflowHidden) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
:global(body) {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
main {
|
||||
overflow-y: auto;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
@ -125,5 +118,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte'
|
||||
import { tick } from 'svelte'
|
||||
|
||||
import { afterNavigate, disableScrollHandling, goto } from '$app/navigation'
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { isErrorLike } from '$lib/common'
|
||||
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
|
||||
@ -11,7 +11,6 @@
|
||||
import SidebarToggleButton from '$lib/repo/SidebarToggleButton.svelte'
|
||||
import { sidebarOpen } from '$lib/repo/stores'
|
||||
import Separator, { getSeparatorPosition } from '$lib/Separator.svelte'
|
||||
import { scrollAll } from '$lib/stores'
|
||||
import TabPanel from '$lib/TabPanel.svelte'
|
||||
import Tabs from '$lib/Tabs.svelte'
|
||||
import { Alert } from '$lib/wildcard'
|
||||
@ -67,7 +66,6 @@
|
||||
const fileTreeStore = createFileTreeStore({ fetchFileTreeData: fetchSidebarFileTree })
|
||||
let selectedTab: number | null = null
|
||||
let historyPanel: HistoryPanel
|
||||
let rootElement: HTMLElement | null = null
|
||||
let commitHistory: GitHistory_HistoryConnection | null
|
||||
let lastCommit: LastCommitFragment | null
|
||||
|
||||
@ -94,37 +92,9 @@
|
||||
|
||||
const sidebarSize = getSeparatorPosition('repo-sidebar', 0.2)
|
||||
$: sidebarWidth = `max(200px, ${$sidebarSize * 100}%)`
|
||||
|
||||
onMount(() => {
|
||||
// We want the whole page to be scrollable and hide page and repo navigation
|
||||
scrollAll.set(true)
|
||||
return () => scrollAll.set(false)
|
||||
})
|
||||
|
||||
afterNavigate(() => {
|
||||
// When navigating to a new page we want to ensure two things:
|
||||
// - The file sidebar doesn't move. It feels bad when you clicked on a file entry
|
||||
// and the click target moves away because the page is scrolled all the way to the top.
|
||||
// - The beginning of the content should be visible (e.g. the top of the file or the
|
||||
// top of the file table).
|
||||
// In other words, we want to scroll to the top but not all the way
|
||||
|
||||
// Prevents SvelteKit from resetting the scroll position to the very top of the page
|
||||
disableScrollHandling()
|
||||
|
||||
if (rootElement) {
|
||||
// Because the whole page is scrollable we can get the current scroll position from
|
||||
// the window object
|
||||
const top = rootElement.offsetTop
|
||||
if (window.scrollY > top) {
|
||||
// Reset scroll to top of the content
|
||||
window.scrollTo(0, top)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<section bind:this={rootElement}>
|
||||
<section>
|
||||
<div class="sidebar" class:open={$sidebarOpen} style:min-width={sidebarWidth} style:max-width={sidebarWidth}>
|
||||
<header>
|
||||
<h3>
|
||||
@ -182,9 +152,8 @@
|
||||
section {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--code-bg);
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
@ -205,9 +174,6 @@
|
||||
background-color: var(--body-bg);
|
||||
padding: 0.5rem;
|
||||
padding-bottom: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.main {
|
||||
@ -215,6 +181,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@ -232,8 +199,6 @@
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
background-color: var(--code-bg);
|
||||
--align-tabs: flex-start;
|
||||
border-top: 1px solid var(--border-color);
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { from } from 'rxjs'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
import { goto, preloadData } from '$app/navigation'
|
||||
import { afterNavigate, goto, preloadData } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import CodeMirrorBlob from '$lib/CodeMirrorBlob.svelte'
|
||||
import { isErrorLike, SourcegraphURL, type LineOrPositionOrRange, pluralize } from '$lib/common'
|
||||
@ -23,17 +23,31 @@
|
||||
import { Alert, MenuButton, MenuLink } from '$lib/wildcard'
|
||||
import markdownStyles from '$lib/wildcard/Markdown.module.scss'
|
||||
|
||||
import type { PageData } from './$types'
|
||||
import type { PageData, Snapshot } from './$types'
|
||||
import FileViewModeSwitcher from './FileViewModeSwitcher.svelte'
|
||||
import OpenInCodeHostAction from './OpenInCodeHostAction.svelte'
|
||||
import OpenInEditor from '$lib/repo/open-in-editor/OpenInEditor.svelte'
|
||||
import { toViewMode, ViewMode } from './util'
|
||||
import type { ScrollSnapshot } from '$lib/codemirror/utils'
|
||||
|
||||
export let data: PageData
|
||||
|
||||
export const snapshot: Snapshot<ScrollSnapshot | null> = {
|
||||
capture() {
|
||||
return cmblob?.getScrollSnapshot() ?? null
|
||||
},
|
||||
restore(data) {
|
||||
initialScrollPosition = data
|
||||
},
|
||||
}
|
||||
|
||||
const combinedBlobData = createBlobDataHandler()
|
||||
let selectedPosition: LineOrPositionOrRange | null = null
|
||||
const lineWrap = writable<boolean>(false)
|
||||
let cmblob: CodeMirrorBlob | null = null
|
||||
let blob: Awaited<PageData['blob']> | null = null
|
||||
let highlights: Awaited<PageData['highlights']> = ''
|
||||
let selectedPosition: LineOrPositionOrRange | null = null
|
||||
let initialScrollPosition: ScrollSnapshot | null = null
|
||||
|
||||
$: ({
|
||||
repoURL,
|
||||
@ -45,25 +59,37 @@
|
||||
graphQLClient,
|
||||
blameData,
|
||||
} = data)
|
||||
$: viewMode = toViewMode($page.url.searchParams.get('view'))
|
||||
$: combinedBlobData.set(data.blob, data.highlights)
|
||||
$: ({ blob, highlights, blobPending } = $combinedBlobData)
|
||||
$: isFormatted = !!blob?.richHTML
|
||||
$: fileNotFound = !blob && !blobPending
|
||||
$: fileLoadingError = (!blobPending && !blob && $combinedBlobData.blobError) || null
|
||||
$: showRaw = $page.url.searchParams.get('view') === 'raw'
|
||||
|
||||
$: if (!$combinedBlobData.blobPending) {
|
||||
blob = $combinedBlobData.blob
|
||||
highlights = $combinedBlobData.highlights
|
||||
selectedPosition = SourcegraphURL.from($page.url).lineRange
|
||||
}
|
||||
$: fileNotFound = $combinedBlobData.blobPending ? null : !$combinedBlobData.blob
|
||||
$: fileLoadingError = $combinedBlobData.blobPending ? null : !$combinedBlobData.blob && $combinedBlobData.blobError
|
||||
$: isFormatted = !!$combinedBlobData.blob?.richHTML
|
||||
|
||||
$: viewMode = toViewMode($page.url.searchParams.get('view'))
|
||||
$: showBlame = viewMode === ViewMode.Blame
|
||||
$: showFormatted = isFormatted && viewMode === ViewMode.Default && !showBlame
|
||||
|
||||
$: codeIntelAPI = createCodeIntelAPI({
|
||||
settings: setting => (isErrorLike(settings?.final) ? undefined : settings?.final?.[setting]),
|
||||
requestGraphQL(options) {
|
||||
return from(graphQLClient.query(options.request, options.variables).then(toGraphQLResult))
|
||||
},
|
||||
})
|
||||
$: if (!blobPending) {
|
||||
// Update selected position as soon as blob is loaded
|
||||
selectedPosition = SourcegraphURL.from($page.url).lineRange
|
||||
}
|
||||
|
||||
$: showBlame = viewMode === ViewMode.Blame
|
||||
afterNavigate(event => {
|
||||
// Only restore scroll position when the user used the browser history to navigate back
|
||||
// and forth. When the user reloads the page, in which case SvelteKit will also call
|
||||
// Snapshot.restore, we don't want to restore the scroll position. Instead we want the
|
||||
// selected line (if any) to scroll into view.
|
||||
if (event.type !== 'popstate') {
|
||||
initialScrollPosition = null
|
||||
}
|
||||
})
|
||||
|
||||
function viewModeURL(viewMode: ViewMode) {
|
||||
switch (viewMode) {
|
||||
@ -127,7 +153,7 @@
|
||||
</FileHeader>
|
||||
{/if}
|
||||
|
||||
{#if !blobPending && blob && !blob.binary && !data.compare}
|
||||
{#if blob && !blob.binary && !data.compare}
|
||||
<div class="file-info">
|
||||
<FileViewModeSwitcher
|
||||
aria-label="View mode"
|
||||
@ -149,7 +175,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="content" class:loading={blobPending} class:compare={!!data.compare} class:fileNotFound>
|
||||
<div class="content" class:loading={$combinedBlobData.blobPending} class:compare={!!data.compare} class:fileNotFound>
|
||||
{#if !$combinedBlobData.highlightsPending && $combinedBlobData.highlightsError}
|
||||
<Alert variant="danger">
|
||||
Unable to load syntax highlighting: {$combinedBlobData.highlightsError.message}
|
||||
@ -165,23 +191,32 @@
|
||||
Unable to load iff
|
||||
{/if}
|
||||
{/await}
|
||||
{:else if $combinedBlobData.blob && showFormatted}
|
||||
<div class={`rich ${markdownStyles.markdown}`}>
|
||||
{@html $combinedBlobData.blob.richHTML}
|
||||
</div>
|
||||
{:else if blob}
|
||||
{#if blob.richHTML && !showRaw && !showBlame}
|
||||
<div class={`rich ${markdownStyles.markdown}`}>
|
||||
{@html blob.richHTML}
|
||||
</div>
|
||||
{:else}
|
||||
<!--
|
||||
This ensures that a new CodeMirror instance is created when the file changes.
|
||||
This makes the CodeMirror behavior more predictable and avoids issues with
|
||||
carrying over state from the previous file.
|
||||
Specifically this will make it so that the scroll position is reset to
|
||||
`initialScrollPosition` when the file changes.
|
||||
-->
|
||||
{#key blob.canonicalURL}
|
||||
<CodeMirrorBlob
|
||||
bind:this={cmblob}
|
||||
{initialScrollPosition}
|
||||
blobInfo={{
|
||||
...blob,
|
||||
revision: revision ?? '',
|
||||
repoName,
|
||||
commitID,
|
||||
repoName: repoName,
|
||||
revision: revision ?? '',
|
||||
filePath,
|
||||
}}
|
||||
{highlights}
|
||||
{showBlame}
|
||||
blameData={$blameData}
|
||||
{highlights}
|
||||
wrapLines={$lineWrap}
|
||||
selectedLines={selectedPosition?.line ? selectedPosition : null}
|
||||
on:selectline={({ detail: range }) => {
|
||||
@ -193,18 +228,16 @@
|
||||
}}
|
||||
{codeIntelAPI}
|
||||
/>
|
||||
{/if}
|
||||
{:else if !blobPending}
|
||||
{#if fileLoadingError}
|
||||
<Alert variant="danger">
|
||||
Unable to load file data: {fileLoadingError.message}
|
||||
</Alert>
|
||||
{:else if fileNotFound}
|
||||
<div class="circle">
|
||||
<Icon svgPath={mdiMapSearch} size={80} />
|
||||
</div>
|
||||
<h2>File not found</h2>
|
||||
{/if}
|
||||
{/key}
|
||||
{:else if fileLoadingError}
|
||||
<Alert variant="danger">
|
||||
Unable to load file data: {fileLoadingError.message}
|
||||
</Alert>
|
||||
{:else if fileNotFound}
|
||||
<div class="circle">
|
||||
<Icon svgPath={mdiMapSearch} size={80} />
|
||||
</div>
|
||||
<h2>File not found</h2>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@ -212,7 +245,7 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: auto;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
|
||||
&.compare {
|
||||
@ -240,7 +273,6 @@
|
||||
|
||||
.rich {
|
||||
padding: 1rem;
|
||||
overflow: auto;
|
||||
max-width: 50rem;
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,12 @@ test.beforeEach(({ sg }) => {
|
||||
{
|
||||
canonicalURL: `/${repoName}/-/blob/src/index.js`,
|
||||
},
|
||||
{
|
||||
canonicalURL: `/${repoName}/-/blob/src/large-file-1.js`,
|
||||
},
|
||||
{
|
||||
canonicalURL: `/${repoName}/-/blob/src/large-file-2.js`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -39,6 +45,28 @@ test.beforeEach(({ sg }) => {
|
||||
richHTML: '',
|
||||
content: '"file content"',
|
||||
},
|
||||
{
|
||||
__typename: 'GitBlob',
|
||||
name: 'large-file-1.js',
|
||||
path: 'src/large-file-1.js',
|
||||
canonicalURL: `/${repoName}/-/blob/src/large-file-1.js`,
|
||||
isDirectory: false,
|
||||
languages: ['JavaScript'],
|
||||
richHTML: '',
|
||||
content: Array.from({ length: 500 }, (_, i) => `// line ${i + 1};`).join('\n'),
|
||||
totalLines: 500,
|
||||
},
|
||||
{
|
||||
__typename: 'GitBlob',
|
||||
name: 'large-file-2.js',
|
||||
path: 'src/large-file-2.js',
|
||||
canonicalURL: `/${repoName}/-/blob/src/large-file-2.js`,
|
||||
isDirectory: false,
|
||||
languages: ['JavaScript'],
|
||||
richHTML: '',
|
||||
content: Array.from({ length: 300 }, (_, i) => `// line ${i + 1};`).join('\n'),
|
||||
totalLines: 300,
|
||||
},
|
||||
])
|
||||
|
||||
sg.mockOperations({
|
||||
@ -50,7 +78,7 @@ test.beforeEach(({ sg }) => {
|
||||
},
|
||||
},
|
||||
}),
|
||||
TreeEntries: ({}) => ({
|
||||
TreeEntries: ({ repoName }) => ({
|
||||
repository: {
|
||||
commit: {
|
||||
tree: {
|
||||
@ -59,11 +87,11 @@ test.beforeEach(({ sg }) => {
|
||||
},
|
||||
},
|
||||
}),
|
||||
BlobPageQuery: ({}) => ({
|
||||
BlobPageQuery: ({ path, repoName }) => ({
|
||||
repository: {
|
||||
commit: {
|
||||
blob: {
|
||||
canonicalURL: `/${repoName}/-/blob/src/index.js`,
|
||||
canonicalURL: `/${repoName}/-/blob/${path}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -215,6 +243,108 @@ test.describe('file header', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('scroll behavior', () => {
|
||||
const url = `/${repoName}/-/blob/src/large-file-1.js`
|
||||
|
||||
test('initial page load', async ({ page }) => {
|
||||
await page.goto(url)
|
||||
await expect(page.getByText('line 1;'), 'file is scrolled to the top').toBeVisible()
|
||||
})
|
||||
|
||||
test('initial page load with selected line', async ({ page }) => {
|
||||
await page.goto(url + '?L100')
|
||||
const selectedLine = page.getByTestId('selected-line')
|
||||
await expect(selectedLine, 'selected line is scrolled into view').toBeVisible()
|
||||
await expect(selectedLine).toHaveText(/line 100;/)
|
||||
})
|
||||
|
||||
test('go to another file', async ({ page, utils }) => {
|
||||
await page.goto(url)
|
||||
// Scroll to some arbitrary position
|
||||
await utils.scrollYAt(page.getByText('line 1;'), 1000)
|
||||
|
||||
await page.getByRole('link', { name: 'large-file-2.js' }).click()
|
||||
await expect(page.getByText('line 1;')).toBeVisible()
|
||||
})
|
||||
|
||||
test('select a line', async ({ page, utils }) => {
|
||||
await page.goto(url)
|
||||
|
||||
// Scrolls to line 64 at the top (found out by inspecting the test)
|
||||
await utils.scrollYAt(page.getByText('line 1;'), 1000)
|
||||
const line64 = page.getByText('line 64;')
|
||||
await expect(line64).toBeVisible()
|
||||
const position = await line64.boundingBox()
|
||||
|
||||
// Select line
|
||||
await page.getByText('80', { exact: true }).click()
|
||||
await expect(page.getByTestId('selected-line')).toHaveText(/line 80;/)
|
||||
|
||||
// Compare positions
|
||||
expect((await line64.boundingBox())?.y, 'selecting a line preserves scroll position').toBe(position?.y)
|
||||
})
|
||||
|
||||
test('[back] preserve scroll position', async ({ page, utils }) => {
|
||||
await page.goto(url)
|
||||
const line1 = page.getByText('line 1;')
|
||||
await expect(line1).toBeVisible()
|
||||
|
||||
// Scrolls to line 64 at the top (found out by inspecting the test)
|
||||
await utils.scrollYAt(line1, 1000)
|
||||
const line64 = page.getByText('line 64;')
|
||||
await expect(line64).toBeVisible()
|
||||
const position = await line64.boundingBox()
|
||||
|
||||
await page.getByRole('link', { name: 'large-file-2.js' }).click()
|
||||
await expect(line1).toBeVisible()
|
||||
|
||||
await page.goBack()
|
||||
expect((await page.getByText('line 64;').boundingBox())?.y, 'restores scroll position on back navigation').toBe(
|
||||
position?.y
|
||||
)
|
||||
})
|
||||
|
||||
test('[forward] preserve scroll position', async ({ page, utils }) => {
|
||||
await page.goto(url)
|
||||
await page.getByRole('link', { name: 'large-file-2.js' }).click()
|
||||
|
||||
const firstLine = page.getByText('line 1;')
|
||||
await expect(firstLine).toBeVisible()
|
||||
|
||||
// Scrolls to line 64 at the top (found out by inspecting the test)
|
||||
await utils.scrollYAt(firstLine, 1000)
|
||||
const line64 = page.getByText('line 64;')
|
||||
await expect(line64).toBeVisible()
|
||||
const position = await line64.boundingBox()
|
||||
|
||||
await page.goBack()
|
||||
await expect(page.getByText('/ large-file-1.js')).toBeVisible()
|
||||
await page.goForward()
|
||||
await expect(page.getByText('/ large-file-2.js')).toBeVisible()
|
||||
|
||||
expect((await line64.boundingBox())?.y, 'restores scroll navigation on forward navigation').toBe(position?.y)
|
||||
})
|
||||
|
||||
test('[back] preserve scroll position with selected line', async ({ page, utils }) => {
|
||||
await page.goto(url + '?L100')
|
||||
const line100 = page.getByText('line 100;')
|
||||
await expect(line100).toBeVisible()
|
||||
|
||||
// Scrolls to line 210 at the top (found out by inspecting the test)
|
||||
await utils.scrollYAt(line100, 2000)
|
||||
const line210 = page.getByText('line 210;')
|
||||
await expect(line210).toBeVisible()
|
||||
const position = await line210.boundingBox()
|
||||
|
||||
await page.getByRole('link', { name: 'large-file-2.js' }).click()
|
||||
await expect(page.getByText('line 1;')).toBeVisible()
|
||||
|
||||
// This should restore the previous scroll position, not go to the selected line
|
||||
await page.goBack()
|
||||
expect((await line210.boundingBox())?.y, 'restores scroll position on back navigation').toBe(position?.y)
|
||||
})
|
||||
})
|
||||
|
||||
test('non-existent file', async ({ page, sg }) => {
|
||||
sg.mockOperations({
|
||||
BlobPageQuery: ({}) => ({
|
||||
|
||||
@ -82,12 +82,13 @@
|
||||
<style lang="scss">
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: var(--body-bg);
|
||||
position: sticky;
|
||||
top: 2.8rem;
|
||||
top: 0;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin: 0;
|
||||
|
||||
@ -50,6 +50,15 @@ export const selectedLines = StateField.define<SelectedLineRange>({
|
||||
create() {
|
||||
return null
|
||||
},
|
||||
compare(a, b) {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
if (!a || !b) {
|
||||
return false
|
||||
}
|
||||
return a.line === b.line && a.endLine === b.endLine
|
||||
},
|
||||
update(value, transaction) {
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(setSelectedLines)) {
|
||||
@ -219,48 +228,48 @@ export const lineScrollEnforcing = Annotation.define<'scroll-enforcing'>()
|
||||
* View plugin responsible for scrolling the selected line(s) into view if/when
|
||||
* necessary.
|
||||
*/
|
||||
const scrollIntoView = ViewPlugin.fromClass(
|
||||
class implements PluginValue {
|
||||
private lastSelectedLines: SelectedLineRange | null = null
|
||||
constructor(private readonly view: EditorView) {
|
||||
this.lastSelectedLines = this.view.state.field(selectedLines)
|
||||
class ScrollIntoView implements PluginValue {
|
||||
private lastSelectedLines: SelectedLineRange | null = null
|
||||
constructor(private readonly view: EditorView, config: SelectableLineNumbersConfig) {
|
||||
this.lastSelectedLines = this.view.state.field(selectedLines)
|
||||
if (!config.skipInitialScrollIntoView) {
|
||||
this.scrollIntoView(this.lastSelectedLines)
|
||||
}
|
||||
}
|
||||
|
||||
public update(update: ViewUpdate): void {
|
||||
const currentSelectedLines = update.state.field(selectedLines)
|
||||
const isForcedScroll = update.transactions.some(
|
||||
transaction => transaction.annotation(lineScrollEnforcing) === 'scroll-enforcing'
|
||||
)
|
||||
public update(update: ViewUpdate): void {
|
||||
const currentSelectedLines = update.state.field(selectedLines)
|
||||
const isForcedScroll = update.transactions.some(
|
||||
transaction => transaction.annotation(lineScrollEnforcing) === 'scroll-enforcing'
|
||||
)
|
||||
|
||||
const hasSelectedLineChanged = isForcedScroll ? true : this.lastSelectedLines !== currentSelectedLines
|
||||
const isExternalTrigger = update.transactions.some(
|
||||
transaction => transaction.annotation(lineSelectionSource) !== 'gutter'
|
||||
)
|
||||
const hasSelectedLineChanged = isForcedScroll ? true : this.lastSelectedLines !== currentSelectedLines
|
||||
const isExternalTrigger = update.transactions.some(
|
||||
transaction => transaction.annotation(lineSelectionSource) !== 'gutter'
|
||||
)
|
||||
|
||||
if (hasSelectedLineChanged && isExternalTrigger) {
|
||||
// Only scroll selected lines into view when the user isn't
|
||||
// currently selecting lines themselves (as indicated by the
|
||||
// presence of the "gutter" annotation). Otherwise, the scroll
|
||||
// position might change while the user is selecting lines.
|
||||
this.lastSelectedLines = currentSelectedLines
|
||||
this.scrollIntoView(currentSelectedLines)
|
||||
}
|
||||
}
|
||||
|
||||
public scrollIntoView(selection: SelectedLineRange): void {
|
||||
if (selection && shouldScrollIntoView(this.view, selection)) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.view.dispatch({
|
||||
effects: EditorView.scrollIntoView(this.view.state.doc.line(selection.line).from, {
|
||||
y: 'center',
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
if (hasSelectedLineChanged && isExternalTrigger) {
|
||||
// Only scroll selected lines into view when the user isn't
|
||||
// currently selecting lines themselves (as indicated by the
|
||||
// presence of the "gutter" annotation). Otherwise, the scroll
|
||||
// position might change while the user is selecting lines.
|
||||
this.lastSelectedLines = currentSelectedLines
|
||||
this.scrollIntoView(currentSelectedLines)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
public scrollIntoView(selection: SelectedLineRange): void {
|
||||
if (selection && shouldScrollIntoView(this.view, selection)) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.view.dispatch({
|
||||
effects: EditorView.scrollIntoView(this.view.state.doc.line(selection.line).from, {
|
||||
y: 'center',
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectedLineNumberTheme = EditorView.theme({
|
||||
'.cm-lineNumbers': {
|
||||
@ -282,6 +291,12 @@ interface SelectableLineNumbersConfig {
|
||||
* In this case `onSelection` will be ignored.
|
||||
*/
|
||||
onLineClick?: (line: number) => void
|
||||
|
||||
/**
|
||||
* If set to true, the initial selection will not be scrolled into view.
|
||||
*/
|
||||
skipInitialScrollIntoView?: boolean
|
||||
|
||||
// todo(fkling): Refactor this logic, maybe move into separate extensions
|
||||
}
|
||||
|
||||
@ -300,7 +315,7 @@ export function selectableLineNumbers(config: SelectableLineNumbersConfig): Exte
|
||||
let dragging = false
|
||||
|
||||
return [
|
||||
scrollIntoView,
|
||||
ViewPlugin.define(view => new ScrollIntoView(view, config)),
|
||||
selectedLines.init(() => config.initialSelection),
|
||||
lineNumbers({
|
||||
domEventHandlers: {
|
||||
|
||||
@ -280,7 +280,7 @@
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.1",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.23.1",
|
||||
"@codemirror/view": "^6.26.3",
|
||||
"@date-fns/utc": "^1.1.1",
|
||||
"@graphiql/react": "^0.10.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
|
||||
@ -21,7 +21,7 @@ importers:
|
||||
version: 3.8.0-alpha.7(graphql-ws@5.15.0)(graphql@15.4.0)(react-dom@18.1.0)(react@18.1.0)
|
||||
'@codemirror/autocomplete':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(@codemirror/language@6.2.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)(@lezer/common@1.0.0)
|
||||
version: 6.1.0(@codemirror/language@6.2.0)(@codemirror/state@6.4.0)(@codemirror/view@6.26.3)(@lezer/common@1.0.0)
|
||||
'@codemirror/commands':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
@ -47,8 +47,8 @@ importers:
|
||||
specifier: ^6.4.0
|
||||
version: 6.4.0
|
||||
'@codemirror/view':
|
||||
specifier: ^6.23.1
|
||||
version: 6.23.1
|
||||
specifier: ^6.26.3
|
||||
version: 6.26.3
|
||||
'@date-fns/utc':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@ -75,7 +75,7 @@ importers:
|
||||
version: 0.0.1
|
||||
'@opencodegraph/codemirror-extension':
|
||||
specifier: ^0.0.1
|
||||
version: 0.0.1(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)(react-dom@18.1.0)(react@18.1.0)
|
||||
version: 0.0.1(@codemirror/state@6.4.0)(@codemirror/view@6.26.3)(react-dom@18.1.0)(react@18.1.0)
|
||||
'@opentelemetry/api':
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
@ -3241,7 +3241,7 @@ packages:
|
||||
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
||||
dev: true
|
||||
|
||||
/@codemirror/autocomplete@6.1.0(@codemirror/language@6.2.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)(@lezer/common@1.0.0):
|
||||
/@codemirror/autocomplete@6.1.0(@codemirror/language@6.2.0)(@codemirror/state@6.4.0)(@codemirror/view@6.26.3)(@lezer/common@1.0.0):
|
||||
resolution: {integrity: sha512-wtO4O5WDyXhhCd4q4utDIDZxnQfmJ++3dGBCG9LMtI79+92OcA1DVk/n7BEupKmjIr8AzvptDz7YQ9ud6OkU+A==}
|
||||
peerDependencies:
|
||||
'@codemirror/language': ^6.0.0
|
||||
@ -3251,7 +3251,7 @@ packages:
|
||||
dependencies:
|
||||
'@codemirror/language': 6.2.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.1
|
||||
'@codemirror/view': 6.26.3
|
||||
'@lezer/common': 1.0.0
|
||||
dev: false
|
||||
|
||||
@ -3260,14 +3260,14 @@ packages:
|
||||
dependencies:
|
||||
'@codemirror/language': 6.2.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.1
|
||||
'@codemirror/view': 6.26.3
|
||||
'@lezer/common': 1.0.0
|
||||
dev: false
|
||||
|
||||
/@codemirror/lang-css@6.0.0(@codemirror/view@6.23.1)(@lezer/common@1.0.0):
|
||||
/@codemirror/lang-css@6.0.0(@codemirror/view@6.26.3)(@lezer/common@1.0.0):
|
||||
resolution: {integrity: sha512-jBqc+BTuwhNOTlrimFghLlSrN6iFuE44HULKWoR4qKYObhOIl9Lci1iYj6zMIte1XTQmZguNvjXMyr43LUKwSw==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.1.0(@codemirror/language@6.2.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)(@lezer/common@1.0.0)
|
||||
'@codemirror/autocomplete': 6.1.0(@codemirror/language@6.2.0)(@codemirror/state@6.4.0)(@codemirror/view@6.26.3)(@lezer/common@1.0.0)
|
||||
'@codemirror/language': 6.2.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@lezer/css': 1.0.0
|
||||
@ -3276,11 +3276,11 @@ packages:
|
||||
- '@lezer/common'
|
||||
dev: false
|
||||
|
||||
/@codemirror/lang-html@6.1.0(@codemirror/view@6.23.1):
|
||||
/@codemirror/lang-html@6.1.0(@codemirror/view@6.26.3):
|
||||
resolution: {integrity: sha512-gA7NmJxqvnhwza05CvR7W/39Ap9r/4Vs9uiC0IeFYo1hSlJzc/8N6Evviz6vTW1x8SpHcRYyqKOf6rpl6LfWtg==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.1.0(@codemirror/language@6.2.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)(@lezer/common@1.0.0)
|
||||
'@codemirror/lang-css': 6.0.0(@codemirror/view@6.23.1)(@lezer/common@1.0.0)
|
||||
'@codemirror/autocomplete': 6.1.0(@codemirror/language@6.2.0)(@codemirror/state@6.4.0)(@codemirror/view@6.26.3)(@lezer/common@1.0.0)
|
||||
'@codemirror/lang-css': 6.0.0(@codemirror/view@6.26.3)(@lezer/common@1.0.0)
|
||||
'@codemirror/lang-javascript': 6.0.1
|
||||
'@codemirror/language': 6.2.0
|
||||
'@codemirror/state': 6.4.0
|
||||
@ -3293,11 +3293,11 @@ packages:
|
||||
/@codemirror/lang-javascript@6.0.1:
|
||||
resolution: {integrity: sha512-kjGbBEosl+ozDU5ruDV48w4v3H6KECTFiDjqMLT0KhVwESPfv3wOvnDrTT0uaMOg3YRGnBWsyiIoKHl/tNWWDg==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.1.0(@codemirror/language@6.2.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)(@lezer/common@1.0.0)
|
||||
'@codemirror/autocomplete': 6.1.0(@codemirror/language@6.2.0)(@codemirror/state@6.4.0)(@codemirror/view@6.26.3)(@lezer/common@1.0.0)
|
||||
'@codemirror/language': 6.2.0
|
||||
'@codemirror/lint': 6.0.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.1
|
||||
'@codemirror/view': 6.26.3
|
||||
'@lezer/common': 1.0.0
|
||||
'@lezer/javascript': 1.0.1
|
||||
dev: false
|
||||
@ -3312,10 +3312,10 @@ packages:
|
||||
/@codemirror/lang-markdown@6.0.0:
|
||||
resolution: {integrity: sha512-ozJaO1W4WgGlwWOoYCSYzbVhhM0YM/4lAWLrNsBbmhh5Ztpl0qm4CgEQRl3t8/YcylTZYBIXiskui8sHNGd4dg==}
|
||||
dependencies:
|
||||
'@codemirror/lang-html': 6.1.0(@codemirror/view@6.23.1)
|
||||
'@codemirror/lang-html': 6.1.0(@codemirror/view@6.26.3)
|
||||
'@codemirror/language': 6.2.0
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.1
|
||||
'@codemirror/view': 6.26.3
|
||||
'@lezer/common': 1.0.0
|
||||
'@lezer/markdown': 1.0.1
|
||||
dev: false
|
||||
@ -3324,7 +3324,7 @@ packages:
|
||||
resolution: {integrity: sha512-tabB0Ef/BflwoEmTB4a//WZ9P90UQyne9qWB9YFsmeS4bnEqSys7UpGk/da1URMXhyfuzWCwp+AQNMhvu8SfnA==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.1
|
||||
'@codemirror/view': 6.26.3
|
||||
'@lezer/common': 1.0.0
|
||||
'@lezer/highlight': 1.0.0
|
||||
'@lezer/lr': 1.2.0
|
||||
@ -3341,7 +3341,7 @@ packages:
|
||||
resolution: {integrity: sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.1
|
||||
'@codemirror/view': 6.26.3
|
||||
crelt: 1.0.5
|
||||
dev: false
|
||||
|
||||
@ -3349,7 +3349,7 @@ packages:
|
||||
resolution: {integrity: sha512-uOinkOrM+daMduCgMPomDfKLr7drGHB4jHl3Vq6xY2WRlL7MkNsBE0b+XHYa/Mee2npsJOgwvkW4n1lMFeBW2Q==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.1
|
||||
'@codemirror/view': 6.26.3
|
||||
crelt: 1.0.5
|
||||
dev: false
|
||||
|
||||
@ -3357,8 +3357,8 @@ packages:
|
||||
resolution: {integrity: sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==}
|
||||
dev: false
|
||||
|
||||
/@codemirror/view@6.23.1:
|
||||
resolution: {integrity: sha512-J2Xnn5lFYT1ZN/5ewEoMBCmLlL71lZ3mBdb7cUEuHhX2ESoSrNEucpsDXpX22EuTGm9LOgC9v4Z0wx+Ez8QmGA==}
|
||||
/@codemirror/view@6.26.3:
|
||||
resolution: {integrity: sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.0
|
||||
style-mod: 4.1.0
|
||||
@ -6159,14 +6159,14 @@ packages:
|
||||
rxjs: 7.8.1
|
||||
dev: false
|
||||
|
||||
/@opencodegraph/codemirror-extension@0.0.1(@codemirror/state@6.4.0)(@codemirror/view@6.23.1)(react-dom@18.1.0)(react@18.1.0):
|
||||
/@opencodegraph/codemirror-extension@0.0.1(@codemirror/state@6.4.0)(@codemirror/view@6.26.3)(react-dom@18.1.0)(react@18.1.0):
|
||||
resolution: {integrity: sha512-n3WP/88qcgeJboqGI7HOfPujkhnTAJ5TGV9IIs03MAyY/V4/+/J8TclhUzjCX5Pu27eoG4RuZlY7379ejUvYkw==}
|
||||
peerDependencies:
|
||||
'@codemirror/state': ^6.2.0
|
||||
'@codemirror/view': ^6.7.2
|
||||
dependencies:
|
||||
'@codemirror/state': 6.4.0
|
||||
'@codemirror/view': 6.23.1
|
||||
'@codemirror/view': 6.26.3
|
||||
'@opencodegraph/client': 0.0.1
|
||||
'@opencodegraph/ui-react': 0.0.1(react-dom@18.1.0)(react@18.1.0)
|
||||
deep-equal: 2.2.3
|
||||
|
||||
Loading…
Reference in New Issue
Block a user