mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 18:51:59 +00:00
Refactor URL helpers (#61290)
* Refactor URL helpers We currently have quite a few URL helpers for which is not obvious how they are supposed to be used. Additionally we often "prettify" certain parts of URL when generating one but that's easy to forget. This commit attempts to improve this situation in various ways: - Reduce the number of helper functions. Instead provide a `SourcegraphURL` class that should be used for parsing, manipulating and converting a URL to a string. - Rename helper functions that operate on `git:` URLs to make their purpose clearer. - Reduce the scope/visibility of certain helpers. * Fix lint * Fix lint issues * Fix lint issues * Fix lint issues * Cleanup * Fix lint issues * More cleanup * Fix lint issues * Remove stray character
This commit is contained in:
parent
7aa0ec0a98
commit
b985fabf4a
@ -3,7 +3,7 @@ import React, { useCallback, type KeyboardEvent, type MouseEvent } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { appendLineRangeQueryParameter, toPositionOrRangeQueryParameter } from '@sourcegraph/common'
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
import type { MatchGroup } from '@sourcegraph/shared/src/components/ranking/PerFileResultRanking'
|
||||
import { type ContentMatch, getFileMatchUrl } from '@sourcegraph/shared/src/search/stream'
|
||||
import type { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
|
||||
@ -27,19 +27,15 @@ export const FileMatchChildren: React.FunctionComponent<React.PropsWithChildren<
|
||||
const { result, grouped, telemetryService } = props
|
||||
|
||||
const createCodeExcerptLink = (group: MatchGroup): string => {
|
||||
const positionOrRangeQueryParameter = toPositionOrRangeQueryParameter({
|
||||
range: {
|
||||
start: {
|
||||
line: group.matches[0].startLine + 1,
|
||||
character: group.matches[0].startCharacter + 1,
|
||||
},
|
||||
end: {
|
||||
line: group.matches[0].endLine + 1,
|
||||
character: group.matches[0].endCharacter + 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
return appendLineRangeQueryParameter(getFileMatchUrl(result), positionOrRangeQueryParameter)
|
||||
const match = group.matches[0]
|
||||
return SourcegraphURL.from(getFileMatchUrl(result))
|
||||
.setLineRange({
|
||||
line: match.startLine + 1,
|
||||
character: match.startCharacter + 1,
|
||||
endLine: match.endLine + 1,
|
||||
endCharacter: match.endCharacter + 1,
|
||||
})
|
||||
.toString()
|
||||
}
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
@ -282,6 +282,7 @@ ts_project(
|
||||
"src/shared/code-hosts/shared/ViewOnSourcegraphButton.test.tsx",
|
||||
"src/shared/code-hosts/shared/codeHost.test.tsx",
|
||||
"src/shared/code-hosts/shared/codeViews.test.ts",
|
||||
"src/shared/code-hosts/shared/util/selections.test.ts",
|
||||
"src/shared/code-hosts/shared/views.test.ts",
|
||||
"src/shared/code-hosts/sourcegraph/inject.test.tsx",
|
||||
"src/testSetup.test.ts",
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import type { TextDocumentPositionParameters } from '@sourcegraph/client-api'
|
||||
import { type AbsoluteRepoFilePosition, toURIWithPath } from '@sourcegraph/shared/src/util/url'
|
||||
import { type AbsoluteRepoFilePosition, makeRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
export const toTextDocumentPositionParameters = (
|
||||
position: AbsoluteRepoFilePosition
|
||||
): TextDocumentPositionParameters => ({
|
||||
textDocument: {
|
||||
uri: toURIWithPath(position),
|
||||
uri: makeRepoGitURI({
|
||||
repoName: position.repoName,
|
||||
filePath: position.filePath,
|
||||
commitID: position.commitID,
|
||||
revision: position.revision,
|
||||
}),
|
||||
},
|
||||
position: {
|
||||
character: position.position.character - 1,
|
||||
|
||||
@ -56,7 +56,6 @@ import {
|
||||
registerHighlightContributions,
|
||||
isExternalLink,
|
||||
type LineOrPositionOrRange,
|
||||
lprToSelectionsZeroIndexed,
|
||||
} from '@sourcegraph/common'
|
||||
import type { WorkspaceRoot } from '@sourcegraph/extension-api-types'
|
||||
import { gql, isHTTPAuthError } from '@sourcegraph/http-client'
|
||||
@ -82,9 +81,8 @@ import {
|
||||
type RepoSpec,
|
||||
type ResolvedRevisionSpec,
|
||||
type RevisionSpec,
|
||||
toRootURI,
|
||||
toURIWithPath,
|
||||
type ViewStateSpec,
|
||||
makeRepoGitURI,
|
||||
} from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import { background } from '../../../browser-extension/web-extension-api/runtime'
|
||||
@ -114,6 +112,7 @@ import { NotAuthenticatedError, RepoURLParseError } from './errors'
|
||||
import { initializeExtensions } from './extensions'
|
||||
import { SignInButton } from './SignInButton'
|
||||
import { resolveRepoNamesForDiffOrFileInfo, defaultRevisionToCommitID } from './util/fileInfo'
|
||||
import { lprToSelectionsZeroIndexed } from './util/selections'
|
||||
import {
|
||||
type ViewOnSourcegraphButtonClassProps,
|
||||
ViewOnSourcegraphButton,
|
||||
@ -1032,9 +1031,14 @@ export async function handleCodeHost({
|
||||
} = codeViewEvent
|
||||
|
||||
const initializeModelAndViewerForFileInfo = async (
|
||||
fileInfo: FileInfoWithContent & FileInfoWithRepoName
|
||||
fileInfo: FileInfoWithContent
|
||||
): Promise<CodeEditorWithPartialModel> => {
|
||||
const uri = toURIWithPath(fileInfo)
|
||||
const uri = makeRepoGitURI({
|
||||
repoName: fileInfo.repoName,
|
||||
commitID: fileInfo.commitID,
|
||||
revision: fileInfo.revision,
|
||||
filePath: fileInfo.filePath,
|
||||
})
|
||||
|
||||
// Model
|
||||
const languageId = getModeFromPath(fileInfo.filePath)
|
||||
@ -1052,7 +1056,11 @@ export async function handleCodeHost({
|
||||
|
||||
const extensionHostAPI = await extensionsController.extHostAPI
|
||||
|
||||
const rootURI = toRootURI(fileInfo)
|
||||
const rootURI = makeRepoGitURI({
|
||||
repoName: fileInfo.repoName,
|
||||
commitID: fileInfo.commitID,
|
||||
revision: fileInfo.revision,
|
||||
})
|
||||
const [, viewerId] = await Promise.all([
|
||||
// Only add the model if it doesn't exist
|
||||
// (there may be several code views on the page pointing to the same model)
|
||||
|
||||
@ -0,0 +1,137 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import { lprToSelectionsZeroIndexed } from './selections'
|
||||
|
||||
/**
|
||||
* Asserts deep object equality using node's assert.deepEqual, except it (1) ignores differences in the
|
||||
* prototype (because that causes 2 object literals to fail the test) and (2) treats undefined properties as
|
||||
* missing.
|
||||
*/
|
||||
function assertDeepStrictEqual(actual: any, expected: any): void {
|
||||
actual = JSON.parse(JSON.stringify(actual))
|
||||
expected = JSON.parse(JSON.stringify(expected))
|
||||
expect(actual).toEqual(expected)
|
||||
}
|
||||
|
||||
describe('lprToSelectionsZeroIndexed', () => {
|
||||
test('converts an LPR with only a start line', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 5,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
end: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
anchor: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
active: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('converts an LPR with a line and a character', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 5,
|
||||
character: 45,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
end: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
anchor: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
active: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('converts an LPR with a start and end line', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 12,
|
||||
endLine: 15,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 11,
|
||||
character: 0,
|
||||
},
|
||||
end: {
|
||||
line: 14,
|
||||
character: 0,
|
||||
},
|
||||
anchor: {
|
||||
line: 11,
|
||||
character: 0,
|
||||
},
|
||||
active: {
|
||||
line: 14,
|
||||
character: 0,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('converts an LPR with a start and end line and characters', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 12,
|
||||
character: 30,
|
||||
endLine: 15,
|
||||
endCharacter: 60,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 11,
|
||||
character: 29,
|
||||
},
|
||||
end: {
|
||||
line: 14,
|
||||
character: 59,
|
||||
},
|
||||
anchor: {
|
||||
line: 11,
|
||||
character: 29,
|
||||
},
|
||||
active: {
|
||||
line: 14,
|
||||
character: 59,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -2,12 +2,45 @@ import { isEqual } from 'lodash'
|
||||
import { fromEvent, type Observable } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
|
||||
import { lprToSelectionsZeroIndexed } from '@sourcegraph/common'
|
||||
import type { Selection } from '@sourcegraph/extension-api-types'
|
||||
import { parseHash } from '@sourcegraph/shared/src/util/url'
|
||||
import { LineOrPositionOrRange, SourcegraphURL } from '@sourcegraph/common'
|
||||
import type { Position, Selection, Range } from '@sourcegraph/extension-api-types'
|
||||
|
||||
function lprToRange(lpr: LineOrPositionOrRange): Range | undefined {
|
||||
if (lpr.line === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
start: { line: lpr.line, character: lpr.character || 0 },
|
||||
end: {
|
||||
line: lpr.endLine || lpr.line,
|
||||
character: lpr.endCharacter || lpr.character || 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// `lprToRange` sets character to 0 if it's undefined. Only - 1 the character if it's not 0.
|
||||
const characterZeroIndexed = (character: number): number => (character === 0 ? character : character - 1)
|
||||
|
||||
export function lprToSelectionsZeroIndexed(lpr: LineOrPositionOrRange): Selection[] {
|
||||
const range = lprToRange(lpr)
|
||||
if (range === undefined) {
|
||||
return []
|
||||
}
|
||||
const start: Position = { line: range.start.line - 1, character: characterZeroIndexed(range.start.character) }
|
||||
const end: Position = { line: range.end.line - 1, character: characterZeroIndexed(range.end.character) }
|
||||
return [
|
||||
{
|
||||
start,
|
||||
end,
|
||||
anchor: start,
|
||||
active: end,
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function getSelectionsFromHash(): Selection[] {
|
||||
return lprToSelectionsZeroIndexed(parseHash(window.location.hash))
|
||||
return lprToSelectionsZeroIndexed(SourcegraphURL.from({ hash: window.location.hash }).lineRange)
|
||||
}
|
||||
|
||||
export function observeSelectionsFromHash(): Observable<Selection[]> {
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
import type { PlatformContext } from '@sourcegraph/shared/src/platform/context'
|
||||
import {
|
||||
type FileSpec,
|
||||
makeRepoURI,
|
||||
makeRepoGitURI,
|
||||
type RawRepoSpec,
|
||||
type RepoSpec,
|
||||
type ResolvedRevisionSpec,
|
||||
@ -161,7 +161,7 @@ export const resolveRevision = memoizeObservable(
|
||||
return repository.commit.oid
|
||||
})
|
||||
),
|
||||
makeRepoURI
|
||||
makeRepoGitURI
|
||||
)
|
||||
|
||||
export function retryWhenCloneInProgressError<T>(): (v: Observable<T>) => Observable<T> {
|
||||
@ -241,5 +241,5 @@ export const fetchBlobContentLines = memoizeObservable(
|
||||
return repository.commit.file.content.split('\n')
|
||||
})
|
||||
),
|
||||
makeRepoURI
|
||||
makeRepoGitURI
|
||||
)
|
||||
|
||||
@ -56,7 +56,6 @@ ts_project(
|
||||
],
|
||||
tsconfig = ":tsconfig",
|
||||
deps = [
|
||||
":node_modules/@sourcegraph/extension-api-types",
|
||||
"//:node_modules/@types/dompurify",
|
||||
"//:node_modules/@types/highlight.js",
|
||||
"//:node_modules/@types/history",
|
||||
|
||||
@ -1,17 +1,130 @@
|
||||
import { describe, expect, it, test } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { lprToSelectionsZeroIndexed, encodeURIPathComponent, appendLineRangeQueryParameter } from './url'
|
||||
import { encodeURIPathComponent, LineOrPositionOrRange, SourcegraphURL } from './url'
|
||||
|
||||
/**
|
||||
* Asserts deep object equality using node's assert.deepEqual, except it (1) ignores differences in the
|
||||
* prototype (because that causes 2 object literals to fail the test) and (2) treats undefined properties as
|
||||
* missing.
|
||||
*/
|
||||
function assertDeepStrictEqual(actual: any, expected: any): void {
|
||||
actual = JSON.parse(JSON.stringify(actual))
|
||||
expected = JSON.parse(JSON.stringify(expected))
|
||||
expect(actual).toEqual(expected)
|
||||
}
|
||||
describe('SourcegraphURL', () => {
|
||||
describe('from', () => {
|
||||
describe('string input', () => {
|
||||
it.each`
|
||||
input | expected
|
||||
${'https://sourcegraph.com/some/path?some=param#L1'} | ${'https://sourcegraph.com/some/path?some=param#L1'}
|
||||
${'https://sourcegraph.com:3443/some/path?some=param#L1'} | ${'https://sourcegraph.com:3443/some/path?some=param#L1'}
|
||||
${'/some/path?some=param#L1'} | ${'/some/path?some=param#L1'}
|
||||
${'?some=param#L1'} | ${'?some=param#L1'}
|
||||
${'#L1'} | ${'#L1'}
|
||||
`('$input => $expected', ({ input, expected }) => {
|
||||
expect(SourcegraphURL.from(input).toString()).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts a URL object', () => {
|
||||
expect(SourcegraphURL.from(new URL('https://sourcegraph.com/some/path?some=param#L1')).toString()).toBe(
|
||||
'https://sourcegraph.com/some/path?some=param#L1'
|
||||
)
|
||||
})
|
||||
|
||||
it('accepts URLSearchParams', () => {
|
||||
expect(SourcegraphURL.from(new URLSearchParams('some=param')).search).toBe('?some=param')
|
||||
})
|
||||
|
||||
describe('location object', () => {
|
||||
it.each`
|
||||
pathname | search | hash | expected
|
||||
${'/some/path'} | ${'?some=param'} | ${'#L1'} | ${'/some/path?some=param#L1'}
|
||||
${'/some/path'} | ${'some=param'} | ${'L1'} | ${'/some/path?some=param#L1'}
|
||||
${'/some/path'} | ${''} | ${''} | ${'/some/path'}
|
||||
${''} | ${'?some=param'} | ${'#L1'} | ${'?some=param#L1'}
|
||||
${''} | ${''} | ${'#L1'} | ${'#L1'}
|
||||
${'/some/path'} | ${''} | ${'#L1'} | ${'/some/path#L1'}
|
||||
`(
|
||||
'{pathname: $pathname, search: $search, hash: $hash} => $expected',
|
||||
({ pathname, search, hash, expected }) => {
|
||||
expect(SourcegraphURL.from({ pathname, search, hash }).toString()).toBe(expected)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('get lineRange', () => {
|
||||
it.each`
|
||||
input | expected
|
||||
${'L1'} | ${{ line: 1 }}
|
||||
${'L1:0'} | ${{ line: 1 }}
|
||||
${'L1:1'} | ${{ line: 1, character: 1 }}
|
||||
${'L1-2'} | ${{ line: 1, endLine: 2 }}
|
||||
${'L1:1-2:2'} | ${{ line: 1, character: 1, endLine: 2, endCharacter: 2 }}
|
||||
${'L1:1-1:1'} | ${{ line: 1, character: 1 }}
|
||||
`('$input => $expected', ({ input, expected }) => {
|
||||
// Search parameter position
|
||||
expect(SourcegraphURL.from(`/some/path?${input}`).lineRange).toEqual(expected)
|
||||
// Hash position
|
||||
expect(SourcegraphURL.from(`/some/path#${input}`).lineRange).toEqual(expected)
|
||||
})
|
||||
|
||||
it.each`
|
||||
input | message
|
||||
${'L-1'} | ${'invalid line number'}
|
||||
${'L0'} | ${'invalid line number'}
|
||||
${'L1:1-1'} | ${'invalid position-line range'}
|
||||
${'L1-2:1'} | ${'invalid line-position range'}
|
||||
${'L1:1-2:1-3:1'} | ${'multiple ranges'}
|
||||
`('$input ($message) => {}', ({ input }) => {
|
||||
// Search parameter position
|
||||
expect(SourcegraphURL.from(`/some/path?${input}`).lineRange).toEqual({})
|
||||
// Hash position
|
||||
expect(SourcegraphURL.from(`/some/path#${input}`).lineRange).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setLineRange', () => {
|
||||
it.each`
|
||||
input | lpr | expected
|
||||
${'/path'} | ${{ line: 24, character: 24 }} | ${'/path?L24:24'}
|
||||
${'/path'} | ${{ line: 12, endLine: 56 }} | ${'/path?L12-56'}
|
||||
${'/path'} | ${{ line: 12, character: 3, endLine: 56, endCharacter: 1 }} | ${'/path?L12:3-56:1'}
|
||||
${'/path'} | ${{ line: 12, character: 0, endLine: 56, endCharacter: 0 }} | ${'/path?L12-56'}
|
||||
${'/path?test=test'} | ${{ line: 24, character: 24 }} | ${'/path?L24:24&test=test'}
|
||||
${'/path?L1:1'} | ${{ line: 24, character: 24 }} | ${'/path?L24:24'}
|
||||
${'/path?L1:1&test=test'} | ${{}} | ${'/path?test=test'}
|
||||
${'?'} | ${{ line: 24, character: 24 }} | ${'?L24:24'}
|
||||
${'?'} | ${{ line: 24, endLine: 56 }} | ${'?L24-56'}
|
||||
${'?'} | ${{ line: 12, character: 3, endLine: 56, endCharacter: 1 }} | ${'?L12:3-56:1'}
|
||||
${'?'} | ${{ line: 12, character: 0, endLine: 56, endCharacter: 0 }} | ${'?L12-56'}
|
||||
${'?test=test'} | ${{ line: 24, character: 24 }} | ${'?L24:24&test=test'}
|
||||
${'?L1:1'} | ${{ line: 24, character: 24 }} | ${'?L24:24'}
|
||||
${'?L1:1&test=test'} | ${{}} | ${'?test=test'}
|
||||
${'?L1:1'} | ${{}} | ${''}
|
||||
${'?L1:1'} | ${null} | ${''}
|
||||
`('$input => $expected', ({ input, lpr, expected }) => {
|
||||
expect(SourcegraphURL.from(input).setLineRange(lpr).toString()).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('get viewState', () => {
|
||||
it.each`
|
||||
input | expected
|
||||
${'/path#tab=references'} | ${'references'}
|
||||
${'/path#test=test&tab=references'} | ${'references'}
|
||||
${'/path?test=test#tab=references'} | ${'references'}
|
||||
${'/path?L1:1#tab=references'} | ${'references'}
|
||||
`('$input => $expected', ({ input, expected }) => {
|
||||
expect(SourcegraphURL.from(input).viewState).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setViewState', () => {
|
||||
it.each`
|
||||
input | viewState | expected
|
||||
${'/path'} | ${'references'} | ${'/path#tab=references'}
|
||||
${'/path#test=test'} | ${'references'} | ${'/path#test=test&tab=references'}
|
||||
${'/path#tab=references'} | ${'definitions'} | ${'/path#tab=definitions'}
|
||||
${'/path?test=test'} | ${'references'} | ${'/path?test=test#tab=references'}
|
||||
${'/path?L1:1'} | ${'references'} | ${'/path?L1:1#tab=references'}
|
||||
`('$input => $expected', ({ input, viewState, expected }) => {
|
||||
expect(SourcegraphURL.from(input).setViewState(viewState).toString()).toBe(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('encodeURIPathComponent', () => {
|
||||
it('encodes all special characters except slashes and the plus sign', () => {
|
||||
@ -21,136 +134,56 @@ describe('encodeURIPathComponent', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('lprToSelectionsZeroIndexed', () => {
|
||||
test('converts an LPR with only a start line', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 5,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
end: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
anchor: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
active: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
describe('parse legacy hash', () => {
|
||||
function parseHash(hash: string): LineOrPositionOrRange & { viewState?: string } {
|
||||
const url = SourcegraphURL.from({ pathname: '', hash })
|
||||
return { ...url.lineRange, viewState: url.viewState }
|
||||
}
|
||||
|
||||
it('parses empty hash', () => {
|
||||
expect(parseHash('')).toEqual({})
|
||||
})
|
||||
|
||||
test('converts an LPR with a line and a character', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 5,
|
||||
character: 45,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
end: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
anchor: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
active: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
it('parses unexpectedly formatted hash', () => {
|
||||
expect(parseHash('L-53')).toEqual({})
|
||||
expect(parseHash('L53:')).toEqual({})
|
||||
expect(parseHash('L1:2-')).toEqual({})
|
||||
expect(parseHash('L1:2-3')).toEqual({})
|
||||
expect(parseHash('L1:2-3:')).toEqual({})
|
||||
expect(parseHash('L1:-3:')).toEqual({})
|
||||
expect(parseHash('L1:-3:4')).toEqual({})
|
||||
expect(parseHash('L1-2:3')).toEqual({})
|
||||
expect(parseHash('L1-2:')).toEqual({})
|
||||
expect(parseHash('L1:-2')).toEqual({})
|
||||
expect(parseHash('L1:2--3:4')).toEqual({})
|
||||
expect(parseHash('L53:a')).toEqual({})
|
||||
})
|
||||
|
||||
test('converts an LPR with a start and end line', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 12,
|
||||
endLine: 15,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 11,
|
||||
character: 0,
|
||||
},
|
||||
end: {
|
||||
line: 14,
|
||||
character: 0,
|
||||
},
|
||||
anchor: {
|
||||
line: 11,
|
||||
character: 0,
|
||||
},
|
||||
active: {
|
||||
line: 14,
|
||||
character: 0,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
it('parses hash with leading octothorpe', () => {
|
||||
expect(parseHash('#L1')).toEqual({ line: 1 })
|
||||
})
|
||||
|
||||
test('converts an LPR with a start and end line and characters', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 12,
|
||||
character: 30,
|
||||
endLine: 15,
|
||||
endCharacter: 60,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 11,
|
||||
character: 29,
|
||||
},
|
||||
end: {
|
||||
line: 14,
|
||||
character: 59,
|
||||
},
|
||||
anchor: {
|
||||
line: 11,
|
||||
character: 29,
|
||||
},
|
||||
active: {
|
||||
line: 14,
|
||||
character: 59,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('appendLineRangeQueryParameter', () => {
|
||||
it('appends line range to the start of query with existing parameters', () => {
|
||||
expect(
|
||||
appendLineRangeQueryParameter(
|
||||
'/github.com/sourcegraph/sourcegraph/-/blob/.gitattributes?test=test',
|
||||
'L24:24'
|
||||
)
|
||||
).toBe('/github.com/sourcegraph/sourcegraph/-/blob/.gitattributes?L24:24&test=test')
|
||||
it('parses hash with line', () => {
|
||||
expect(parseHash('L1')).toEqual({ line: 1 })
|
||||
})
|
||||
|
||||
it('parses hash with line and character', () => {
|
||||
expect(parseHash('L1:1')).toEqual({ line: 1, character: 1 })
|
||||
})
|
||||
|
||||
it('parses hash with range', () => {
|
||||
expect(parseHash('L1-2')).toEqual({ line: 1, endLine: 2 })
|
||||
expect(parseHash('L1:2-3:4')).toEqual({ line: 1, character: 2, endLine: 3, endCharacter: 4 })
|
||||
expect(parseHash('L47-L55')).toEqual({ line: 47, endLine: 55 })
|
||||
expect(parseHash('L34:2-L38:3')).toEqual({ line: 34, character: 2, endLine: 38, endCharacter: 3 })
|
||||
})
|
||||
|
||||
it('parses hash with references', () => {
|
||||
expect(parseHash('$references')).toEqual({ viewState: 'references' })
|
||||
expect(parseHash('L1:1$references')).toEqual({ line: 1, character: 1, viewState: 'references' })
|
||||
})
|
||||
it('parses modern hash with references', () => {
|
||||
expect(parseHash('tab=references')).toEqual({ viewState: 'references' })
|
||||
expect(parseHash('L1:1&tab=references')).toEqual({ line: 1, character: 1, viewState: 'references' })
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,46 +1,14 @@
|
||||
import type { Position, Range, Selection } from '@sourcegraph/extension-api-types'
|
||||
|
||||
import { tryCatch } from '../errors'
|
||||
|
||||
/**
|
||||
* Provide one.
|
||||
* @param position either 1-indexed partial position
|
||||
* @param range or 1-indexed partial range spec
|
||||
*/
|
||||
export function toPositionOrRangeQueryParameter(context: {
|
||||
position?: { line: number; character?: number }
|
||||
range?: { start: { line: number; character?: number }; end: { line: number; character?: number } }
|
||||
}): string | undefined {
|
||||
if (context.range) {
|
||||
const emptyRange =
|
||||
context.range.start.line === context.range.end.line &&
|
||||
context.range.start.character === context.range.end.character
|
||||
return (
|
||||
'L' +
|
||||
(emptyRange
|
||||
? toPositionHashComponent(context.range.start)
|
||||
: `${toPositionHashComponent(context.range.start)}-${toPositionHashComponent(context.range.end)}`)
|
||||
)
|
||||
}
|
||||
if (context.position) {
|
||||
return 'L' + toPositionHashComponent(context.position)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ctx 1-indexed partial position
|
||||
*/
|
||||
export function toPositionHashComponent(position: { line: number; character?: number }): string {
|
||||
return position.line.toString() + (position.character ? `:${position.character}` : '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a line, a position, a line range, or a position range. It forbids
|
||||
* just a character, or a range from a line to a position or vice versa (such as
|
||||
* "L1-2:3" or "L1:2-3"), none of which would make much sense.
|
||||
*
|
||||
* 1-indexed.
|
||||
*
|
||||
* For backward compatibility, `character` and `endCharacter` are allowed to be 0,
|
||||
* which is the same as not being set.
|
||||
*/
|
||||
export type LineOrPositionOrRange =
|
||||
| { line?: undefined; character?: undefined; endLine?: undefined; endCharacter?: undefined }
|
||||
@ -48,44 +16,224 @@ export type LineOrPositionOrRange =
|
||||
| { line: number; character?: undefined; endLine?: number; endCharacter?: undefined }
|
||||
| { line: number; character: number; endLine: number; endCharacter: number }
|
||||
|
||||
export function lprToRange(lpr: LineOrPositionOrRange): Range | undefined {
|
||||
if (lpr.line === undefined) {
|
||||
return undefined
|
||||
/**
|
||||
* Parses a string like that encodes a line or a position.
|
||||
* Examples of valid input:
|
||||
* L1 - line: line 1
|
||||
* L1:2 - position: line 1, character 2
|
||||
*
|
||||
* If the line is invalid (e.g. L0), the return value will be an empty object.
|
||||
* If the character is invalid (e.g. L1:0), the return value will be a line.
|
||||
*
|
||||
* @param lineOrPosition a string like that encodes a line or a position
|
||||
* @returns the parsed line or position, or an empty object if the input is invalid
|
||||
*/
|
||||
function parseLineOrPosition(
|
||||
lineOrPosition: string
|
||||
): { line?: undefined; character?: undefined } | { line: number; character?: number } {
|
||||
if (lineOrPosition.startsWith('L')) {
|
||||
lineOrPosition = lineOrPosition.slice(1)
|
||||
}
|
||||
return {
|
||||
start: { line: lpr.line, character: lpr.character || 0 },
|
||||
end: {
|
||||
line: lpr.endLine || lpr.line,
|
||||
character: lpr.endCharacter || lpr.character || 0,
|
||||
},
|
||||
const parts = lineOrPosition.split(':', 2)
|
||||
const line = parts.length >= 1 ? parseInt(parts[0], 10) : undefined
|
||||
const character = parts.length === 2 ? parseInt(parts[1], 10) : undefined
|
||||
|
||||
if (line === undefined || isNaN(line) || line <= 0) {
|
||||
return {}
|
||||
}
|
||||
if (character === undefined || isNaN(character) || character <= 0) {
|
||||
return { line }
|
||||
}
|
||||
return { line, character }
|
||||
}
|
||||
|
||||
// `lprToRange` sets character to 0 if it's undefined. Only - 1 the character if it's not 0.
|
||||
const characterZeroIndexed = (character: number): number => (character === 0 ? character : character - 1)
|
||||
|
||||
export function lprToSelectionsZeroIndexed(lpr: LineOrPositionOrRange): Selection[] {
|
||||
const range = lprToRange(lpr)
|
||||
if (range === undefined) {
|
||||
return []
|
||||
/**
|
||||
* Parses a string like that encodes a line, a position, a line range, or a position range.
|
||||
*
|
||||
* Examples of valid input:
|
||||
* L1 - line: line 1
|
||||
* L1:2 - position: line 1, character 2
|
||||
* L1-2 - line range: line 1 to line 2
|
||||
* L1:2-3:4 - position range: line 1, character 2 to line 3, character 4
|
||||
*
|
||||
* Other combinations of lines or positions are not valid.
|
||||
*
|
||||
* If the range is empty, e.g. L1:2-1:2, the output return value will be simplified to a position, e.g. L1:2.
|
||||
*
|
||||
* @param range a string like that encodes a line, a position, a line range, or a position range
|
||||
* @returns the parsed line, position, line range, or position range, or an empty object if the input is invalid
|
||||
*/
|
||||
function parseLineOrPositionOrRange(range: string): LineOrPositionOrRange {
|
||||
if (!/^(L\d+(:\d+)?(-L?\d+(:\d+)?)?)?$/.test(range)) {
|
||||
return {} // invalid
|
||||
}
|
||||
const start: Position = { line: range.start.line - 1, character: characterZeroIndexed(range.start.character) }
|
||||
const end: Position = { line: range.end.line - 1, character: characterZeroIndexed(range.end.character) }
|
||||
return [
|
||||
{
|
||||
start,
|
||||
end,
|
||||
anchor: start,
|
||||
active: end,
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
|
||||
// Parse the line or position range, ensuring we don't get an inconsistent result
|
||||
// (such as L1-2:3, a range from a line to a position).
|
||||
let line: number | undefined // 17
|
||||
let character: number | undefined // 19
|
||||
let endLine: number | undefined // 21
|
||||
let endCharacter: number | undefined // 23
|
||||
if (range.startsWith('L')) {
|
||||
const positionOrRangeString = range.slice(1)
|
||||
const [startString, endString] = positionOrRangeString.split('-', 2)
|
||||
if (startString) {
|
||||
const parsed = parseLineOrPosition(startString)
|
||||
line = parsed.line
|
||||
character = parsed.character
|
||||
}
|
||||
if (endString) {
|
||||
const parsed = parseLineOrPosition(endString)
|
||||
endLine = parsed.line
|
||||
endCharacter = parsed.character
|
||||
}
|
||||
}
|
||||
if (line === undefined || (endLine !== undefined && typeof character !== typeof endCharacter)) {
|
||||
return {}
|
||||
}
|
||||
if (character === undefined) {
|
||||
return endLine === undefined ? { line } : { line, endLine }
|
||||
}
|
||||
if (endLine === undefined || endCharacter === undefined) {
|
||||
return { line, character }
|
||||
}
|
||||
return simplifyRange({ line, character, endLine, endCharacter })
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplifies a line or a position range. If the input represents an empty range,
|
||||
* e.g. `{ line: 1, character: 2, endLine: 1, endCharacter: 2 }`, the output
|
||||
* will be converted into a position, e.g. `{ line: 1, character: 2 }`.
|
||||
* If the input is invalid, an empty object is returned.
|
||||
*
|
||||
* See {@link LineOrPositionOrRange}.
|
||||
*
|
||||
* @param range the line, position, line range, or position range to simplify
|
||||
* @returns the simplified line, position, line range, or position range
|
||||
*/
|
||||
function simplifyRange(range: LineOrPositionOrRange): LineOrPositionOrRange {
|
||||
if (range.line === undefined) {
|
||||
return {}
|
||||
}
|
||||
// Treat character 0 or endCharacter 0 as 'not set'
|
||||
if (range.character === 0 || range.endCharacter === 0) {
|
||||
return { line: range.line, endLine: range.endLine }
|
||||
}
|
||||
if (range.line === range.endLine && range.character === range.endCharacter) {
|
||||
return { line: range.line, character: range.character }
|
||||
}
|
||||
return range
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a line, a position, a line range, or a position range as a string. The output
|
||||
* is suitable for use in a URL hash or search parameter and can be parsed with
|
||||
* {@link parseLineOrPositionOrRange}.
|
||||
* If the input represents an empty range, e.g. `{ line: 1, character: 2, endLine: 1, endCharacter: 2 }`,
|
||||
* the output will be converted into a position, e.g. `L1:2`.
|
||||
*
|
||||
* See {@link LineOrPositionOrRange}.
|
||||
*
|
||||
* @param lpr the line, position, line range, or position range to format
|
||||
* @returns the formatted line, position, line range, or position range
|
||||
*/
|
||||
function formatLineOrPositionOrRange(lpr: LineOrPositionOrRange): string {
|
||||
lpr = simplifyRange(lpr)
|
||||
if (lpr.line === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (lpr.character === undefined) {
|
||||
return `L${lpr.line}${lpr.endLine ? `-${lpr.endLine}` : ''}`
|
||||
}
|
||||
return `L${lpr.line}:${lpr.character}${lpr.endLine ? `-${lpr.endLine}:${lpr.endCharacter}` : ''}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds, updates or removes a line or position range in search parameters.
|
||||
*
|
||||
* @param inputParams the URL's search parameters
|
||||
* @param lpr the line or position range to add or update
|
||||
* @returns the updated search parameters
|
||||
*/
|
||||
function addOrUpdateLineRange(inputParams: URLSearchParams, lpr: LineOrPositionOrRange | null): URLSearchParams {
|
||||
const params = new URLSearchParams(inputParams)
|
||||
const range = lpr ? formatLineOrPositionOrRange(lpr) : ''
|
||||
|
||||
// Remove existing line range if it exists
|
||||
const existingLineRangeKey = findLineKeyInSearchParameters(params)
|
||||
if (existingLineRangeKey) {
|
||||
params.delete(existingLineRangeKey)
|
||||
}
|
||||
|
||||
return range !== '' ? new URLSearchParams([[range, ''], ...params]) : params
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the given fragment component is a legacy blob hash component or not.
|
||||
* Legacy fragments have the structure `#L<line>:<character>-<line>:<character>$<viewState>`.
|
||||
*
|
||||
* @param hash The URL fragment.
|
||||
*/
|
||||
export function isLegacyFragment(hash: string): boolean {
|
||||
if (hash.startsWith('#')) {
|
||||
hash = hash.slice(1)
|
||||
}
|
||||
return (
|
||||
hash !== '' &&
|
||||
!hash.includes('=') &&
|
||||
(hash.includes('$info') ||
|
||||
hash.includes('$def') ||
|
||||
hash.includes('$references') ||
|
||||
hash.includes('$impl') ||
|
||||
hash.includes('$history'))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the URL fragment (hash) portion, which consists of a line, position, or range in the file, plus an
|
||||
* optional "viewState" parameter (that encodes other view state, such as for the panel).
|
||||
*
|
||||
* For example, in the URL fragment "#L17:19-21:23$foo:bar", the "viewState" is "foo:bar".
|
||||
*
|
||||
* NOTE: Prefer to use {@link SourcegraphURL} instead of this function.
|
||||
*
|
||||
* @template V The type that describes the view state (typically a union of string constants). There is no runtime check that the return value satisfies V.
|
||||
*/
|
||||
function parseHash<V extends string>(hash: string): LineOrPositionOrRange & { viewState?: V } {
|
||||
if (hash.startsWith('#')) {
|
||||
hash = hash.slice('#'.length)
|
||||
}
|
||||
|
||||
if (!isLegacyFragment(hash)) {
|
||||
// Modern hash parsing logic (e.g. for hashes like `"#L17:19-21:23&tab=foo:bar"`:
|
||||
const searchParameters = new URLSearchParams(hash)
|
||||
const existingLineRangeKey = findLineKeyInSearchParameters(searchParameters)
|
||||
const lpr: LineOrPositionOrRange & { viewState?: V } = existingLineRangeKey
|
||||
? parseLineOrPositionOrRange(existingLineRangeKey)
|
||||
: {}
|
||||
if (searchParameters.get('tab')) {
|
||||
lpr.viewState = searchParameters.get('tab') as V
|
||||
}
|
||||
return lpr
|
||||
}
|
||||
|
||||
// Legacy hash parsing logic (e.g. for hashes like "#L17:19-21:23$foo:bar" where the "viewState" is "foo:bar"):
|
||||
if (!/^(L\d+(:\d+)?(-\d+(:\d+)?)?)?(\$.*)?$/.test(hash)) {
|
||||
// invalid or empty hash
|
||||
return {}
|
||||
}
|
||||
const lineCharModalInfo = hash.split('$', 2) // e.g. "L17:19-21:23$references"
|
||||
const lpr: LineOrPositionOrRange & { viewState?: V } = parseLineOrPositionOrRange(lineCharModalInfo[0])
|
||||
if (lineCharModalInfo[1]) {
|
||||
lpr.viewState = lineCharModalInfo[1] as V
|
||||
}
|
||||
return lpr
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an existing line range search parameter like "L1-2:3"
|
||||
*/
|
||||
export function findLineKeyInSearchParameters(searchParameters: URLSearchParams): string | undefined {
|
||||
function findLineKeyInSearchParameters(searchParameters: URLSearchParams): string | undefined {
|
||||
for (const key of searchParameters.keys()) {
|
||||
if (key.startsWith('L')) {
|
||||
return key
|
||||
@ -95,6 +243,245 @@ export function findLineKeyInSearchParameters(searchParameters: URLSearchParams)
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringifies the provided search parameters, replaces encoded `/` and `:` characters,
|
||||
* and removes trailing `=`.
|
||||
*
|
||||
* E.g. L1%3A2 => L1:2
|
||||
*/
|
||||
function formatSearchParameters(searchParameters: string): string {
|
||||
return searchParameters.replaceAll('%2F', '/').replaceAll('%3A', ':').replaceAll('=&', '&').replace(/=$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* This class encapsulates the logic for creating and manipulating Soucegraph URLs.
|
||||
* Not all methods are applicable to all types of URLs. If a method is not applicable
|
||||
* to the URL, the operation will be a no-op.
|
||||
*
|
||||
* See the individual method documentation for details.
|
||||
*
|
||||
* Using this class to manipulate URLs is preferred over manual string manipulation
|
||||
* because it ensures that the URL is prettified when converted to a string.
|
||||
*/
|
||||
export class SourcegraphURL {
|
||||
private url: URL
|
||||
private hasPathname: boolean
|
||||
private hasOrigin: boolean
|
||||
|
||||
private constructor(url: string | URL) {
|
||||
this.url = typeof url === 'string' ? new URL(url, 'http://0.0.0.0/') : new URL(url)
|
||||
this.hasPathname = !(typeof url === 'string' && /^[#?]/.test(url))
|
||||
this.hasOrigin = typeof url !== 'string' || /^https?:/.test(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SourcegraphURL instance from a string, URL, URLSearchParams, or location object.
|
||||
*
|
||||
* When converting the URL back to a string, the string representation depends on the input as well
|
||||
* as the changes that have been made to the URL.
|
||||
*
|
||||
* If the input contains an origin, the output will too.
|
||||
* If the input contains a pathname, the output will too.
|
||||
*
|
||||
* This should make the output fairly predictable.
|
||||
*/
|
||||
public static from(
|
||||
url: string | URL | URLSearchParams | { pathname?: string; search?: string | URLSearchParams; hash?: string }
|
||||
): SourcegraphURL {
|
||||
if (typeof url === 'string' || url instanceof URL) {
|
||||
return new SourcegraphURL(url)
|
||||
}
|
||||
if (url instanceof URLSearchParams) {
|
||||
return new SourcegraphURL(`?${formatSearchParameters(url.toString())}`)
|
||||
}
|
||||
return SourcegraphURL.fromLocation(url)
|
||||
}
|
||||
|
||||
private static fromLocation(location: {
|
||||
pathname?: string
|
||||
search?: string | URLSearchParams
|
||||
hash?: string
|
||||
}): SourcegraphURL {
|
||||
let { pathname = '', search = '', hash = '' } = location
|
||||
if (search) {
|
||||
if (typeof search === 'string') {
|
||||
if (!search.startsWith('?')) {
|
||||
search = `?${search}`
|
||||
}
|
||||
} else {
|
||||
search = `?${search.toString()}`
|
||||
}
|
||||
}
|
||||
if (hash && !hash.startsWith('#')) {
|
||||
hash = `#${hash}`
|
||||
}
|
||||
return new SourcegraphURL(`${pathname}${search}${hash}`)
|
||||
}
|
||||
|
||||
// Mutation methods
|
||||
|
||||
/**
|
||||
* Adds or updates a line or position range in a URL's search parameters.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const url = new SourcegraphURL('/foo?bar')
|
||||
* url.addOrUpdateLineRange({ line: 24, character: 24 })
|
||||
* url.toString() // => '/foo?L24:24&bar'
|
||||
* ```
|
||||
*
|
||||
* @param href the URL to update
|
||||
* @param lpr the line or position range to add or update
|
||||
* @returns the updated URL
|
||||
*/
|
||||
public setLineRange(lpr: LineOrPositionOrRange | null): this {
|
||||
this.url.search = addOrUpdateLineRange(this.url.searchParams, lpr).toString()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the view state, using the modern hash format.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const url = new SourcegraphURL('/foo?bar')
|
||||
* url.setViewState('references')
|
||||
* url.toString() // => '/foo?bar#L1:2-3:4&tab=references'
|
||||
* ```
|
||||
*
|
||||
* @template V The type that describes the view state (typically a union of string constants).
|
||||
* @param viewState the view state to set
|
||||
*/
|
||||
public setViewState<V extends string = string>(viewState: V | undefined): this {
|
||||
// Try to preserve existing hash params
|
||||
const hashParams = new URLSearchParams(this.url.hash.slice(1))
|
||||
if (!viewState && !hashParams.has('tab')) {
|
||||
// Nothing to do
|
||||
return this
|
||||
}
|
||||
if (viewState) {
|
||||
hashParams.set('tab', viewState)
|
||||
} else {
|
||||
hashParams.delete('tab')
|
||||
}
|
||||
this.url.hash = hashParams.toString()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a search parameter to the URL.
|
||||
*/
|
||||
public setSearchParameter(key: string, value: string): this {
|
||||
this.url.searchParams.set(key, value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a search parameter from the URL.
|
||||
*/
|
||||
public deleteSearchParameter(key: string): this {
|
||||
this.url.searchParams.delete(key)
|
||||
return this
|
||||
}
|
||||
|
||||
// Accessors
|
||||
//
|
||||
/**
|
||||
* Parses the encoded line range from the URL's search parameters or hash.
|
||||
* A line range is often present in file URLs to indicate the selected lines or positions.
|
||||
*
|
||||
* If the URL contains a line range in both the search parameters and the hash,
|
||||
* the search parameters take precedence.
|
||||
*
|
||||
* If the line range is "empty" (e.g. L1:2-1:2), the return value will be simplified
|
||||
* to a position (e.g. L1:2).
|
||||
*
|
||||
* Examples of valid line or position ranges:
|
||||
*
|
||||
* ?L1 => { line: 1 }
|
||||
* ?L1:2 => { line: 1, character: 2 }
|
||||
* ?L1-2 => { line: 1, endLine: 2 }
|
||||
* ?L1:2-3:4 => { line: 1, character: 2, endLine: 3, endCharacter: 4 }
|
||||
* ?L1:2-1:2 => { line: 1, character: 2 }
|
||||
* #L1 => { line: 1 }
|
||||
* #L1:2 => { line: 1, character: 2 }
|
||||
* #L1-2 => { line: 1, endLine: 2 }
|
||||
* #L1:2-3:4 => { line: 1, character: 2, endLine: 3, endCharacter: 4 }
|
||||
* #L1:2-1:2 => { line: 1, character: 2 }
|
||||
*
|
||||
* @returns the parsed line or position range, or an empty object if the input is invalid
|
||||
*/
|
||||
public get lineRange(): LineOrPositionOrRange {
|
||||
const existingLineRangeKey = findLineKeyInSearchParameters(this.url.searchParams)
|
||||
if (existingLineRangeKey) {
|
||||
return parseLineOrPositionOrRange(existingLineRangeKey)
|
||||
}
|
||||
return parseHash(this.url.hash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the view state from the URL.
|
||||
*
|
||||
* The view state is often present in file URLs to indicate the selected tab.
|
||||
*
|
||||
* The function supports both legacy and modern hash formats:
|
||||
* - Legacy: `#L1:2-3:4$references`
|
||||
* - Modern: `#L1:2-3:4&tab=references`
|
||||
*
|
||||
* @returns the parsed view state, or undefined if the input is invalid
|
||||
*/
|
||||
public get viewState(): string | undefined {
|
||||
return parseHash(this.url.hash).viewState
|
||||
}
|
||||
|
||||
/**
|
||||
* The pathname of the URL.
|
||||
*/
|
||||
public get pathname(): string {
|
||||
return this.hasPathname ? this.url.pathname : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* The search parameters of the URL.
|
||||
*/
|
||||
public get searchParams(): URLSearchParams {
|
||||
return this.url.searchParams
|
||||
}
|
||||
|
||||
/**
|
||||
* The search parameters of the URL as a string.
|
||||
* Search parameters are prettied.
|
||||
*/
|
||||
public get search(): string {
|
||||
return formatSearchParameters(this.url.search)
|
||||
}
|
||||
|
||||
/**
|
||||
* The hash of the URL.
|
||||
*/
|
||||
public get hash(): string {
|
||||
return formatSearchParameters(this.url.hash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the URL. The output
|
||||
* depends on the original input and the changes that have been
|
||||
* made to the URL.
|
||||
* E.g. if the original input did not include a pathname, the output
|
||||
* won't either.
|
||||
*
|
||||
* The stringified URL is prettified.
|
||||
*/
|
||||
public toString(): string {
|
||||
return (
|
||||
(this.hasOrigin ? this.url.origin : '') +
|
||||
(this.hasPathname ? this.url.pathname : '') +
|
||||
this.search +
|
||||
this.hash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes revision with encodeURIComponent, except that slashes ('/') are preserved,
|
||||
* because they are not ambiguous in any of the current places where used, and URLs
|
||||
@ -104,10 +491,6 @@ export function escapeRevspecForURL(revision: string): string {
|
||||
return encodeURIPathComponent(revision)
|
||||
}
|
||||
|
||||
export function toViewStateHash(viewState: string | undefined): string {
|
||||
return viewState ? `#tab=${viewState}` : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* %-Encodes a path component of a URI.
|
||||
*
|
||||
@ -127,28 +510,3 @@ export const isExternalLink = (
|
||||
windowLocation__testingOnly: Pick<URL, 'origin' | 'href'> = window.location
|
||||
): boolean =>
|
||||
!!tryCatch(() => new URL(url, windowLocation__testingOnly.href).origin !== windowLocation__testingOnly.origin)
|
||||
|
||||
/**
|
||||
* Stringifies the provided search parameters, replaces encoded `/` and `:` characters,
|
||||
* and removes trailing `=`.
|
||||
*/
|
||||
export const formatSearchParameters = (searchParameters: URLSearchParams): string =>
|
||||
searchParameters.toString().replaceAll('%2F', '/').replaceAll('%3A', ':').replaceAll('=&', '&').replace(/=$/, '')
|
||||
|
||||
export const addLineRangeQueryParameter = (
|
||||
searchParameters: URLSearchParams,
|
||||
range: string | undefined
|
||||
): URLSearchParams => {
|
||||
const existingLineRangeKey = findLineKeyInSearchParameters(searchParameters)
|
||||
if (existingLineRangeKey) {
|
||||
searchParameters.delete(existingLineRangeKey)
|
||||
}
|
||||
// If a non-empty range exists add it to the start of the parameters, otherwise return the existing search parameters
|
||||
return range ? new URLSearchParams([[range, ''], ...searchParameters.entries()]) : searchParameters
|
||||
}
|
||||
|
||||
export const appendLineRangeQueryParameter = (url: string, range: string | undefined): string => {
|
||||
const newUrl = new URL(url, window.location.href)
|
||||
const searchQuery = formatSearchParameters(addLineRangeQueryParameter(newUrl.searchParams, range))
|
||||
return newUrl.pathname + `?${searchQuery}` + newUrl.hash
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import type { Context } from '@sourcegraph/template-parser'
|
||||
|
||||
import type { ReferenceContext, DocumentSelector } from '../../codeintel/legacy-extensions/api'
|
||||
import { getModeFromPath } from '../../languages'
|
||||
import { parseRepoURI } from '../../util/url'
|
||||
import { parseRepoGitURI } from '../../util/url'
|
||||
import { match } from '../client/types/textDocument'
|
||||
import type { FlatExtensionHostAPI } from '../contract'
|
||||
import type { ExtensionViewer, ViewerId, ViewerWithPartialModel } from '../viewerTypes'
|
||||
@ -379,7 +379,7 @@ export function providersForDocument<P>(
|
||||
return entries.filter(provider =>
|
||||
match(selector(provider), {
|
||||
uri: document.uri,
|
||||
languageId: getModeFromPath(parseRepoURI(document.uri).filePath || ''),
|
||||
languageId: getModeFromPath(parseRepoGitURI(document.uri).filePath || ''),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
HighlightResponseFormat,
|
||||
} from '../graphql-operations'
|
||||
import type { PlatformContext } from '../platform/context'
|
||||
import { makeRepoURI } from '../util/url'
|
||||
import { makeRepoGitURI } from '../util/url'
|
||||
|
||||
/*
|
||||
Highlighted file result query doesn't support `format` on Sourcegraph versions older than 3.43.
|
||||
@ -127,7 +127,7 @@ export const fetchHighlightedFileLineRanges = memoizeObservable(
|
||||
)
|
||||
},
|
||||
context =>
|
||||
makeRepoURI(context) +
|
||||
makeRepoGitURI(context) +
|
||||
`?disableTimeout=${String(context.disableTimeout)}&ranges=${context.ranges
|
||||
.map(range => `${range.startLine}:${range.endLine}`)
|
||||
.join(',')}&format=${context.format}`
|
||||
|
||||
@ -6,7 +6,7 @@ import { dataOrThrowErrors, gql } from '@sourcegraph/http-client'
|
||||
|
||||
import type { ResolveRawRepoNameResult, TreeEntriesResult, TreeFields } from '../graphql-operations'
|
||||
import type { PlatformContext } from '../platform/context'
|
||||
import { type AbsoluteRepoFile, makeRepoURI, type RepoSpec } from '../util/url'
|
||||
import { type AbsoluteRepoFile, makeRepoGitURI, type RepoSpec } from '../util/url'
|
||||
|
||||
import { CloneInProgressError, RepoNotFoundError } from './errors'
|
||||
|
||||
@ -103,5 +103,5 @@ export const fetchTreeEntries = memoizeObservable(
|
||||
return data.repository.commit.tree
|
||||
})
|
||||
),
|
||||
({ first, requestGraphQL, ...args }) => `${makeRepoURI(args)}:first-${String(first)}`
|
||||
({ first, requestGraphQL, ...args }) => `${makeRepoGitURI(args)}:first-${String(first)}`
|
||||
)
|
||||
|
||||
@ -18,7 +18,7 @@ import type { CodeIntelExtensionHostAPI, FlatExtensionHostAPI, ScipParameters }
|
||||
import { proxySubscribable } from '../api/extension/api/common'
|
||||
import { toPosition } from '../api/extension/api/types'
|
||||
import { getModeFromPath } from '../languages'
|
||||
import { parseRepoURI } from '../util/url'
|
||||
import { parseRepoGitURI } from '../util/url'
|
||||
|
||||
import type { DocumentSelector, TextDocument, DocumentHighlight } from './legacy-extensions/api'
|
||||
import * as sourcegraph from './legacy-extensions/api'
|
||||
@ -143,7 +143,7 @@ function requestFor(textParameters: TextDocumentPositionParameters): LanguageReq
|
||||
function toTextDocument(textDocument: TextDocumentIdentifier): sourcegraph.TextDocument {
|
||||
return {
|
||||
uri: textDocument.uri,
|
||||
languageId: getModeFromPath(parseRepoURI(textDocument.uri).filePath || ''),
|
||||
languageId: getModeFromPath(parseRepoGitURI(textDocument.uri).filePath || ''),
|
||||
text: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { parseRepoURI } from '../../../util/url'
|
||||
import { parseRepoGitURI } from '../../../util/url'
|
||||
|
||||
/**
|
||||
* Extracts the components of a text document URI.
|
||||
@ -6,7 +6,7 @@ import { parseRepoURI } from '../../../util/url'
|
||||
* @param uri The text document URL.
|
||||
*/
|
||||
export function parseGitURI(uri: string): { repo: string; commit: string; path: string } {
|
||||
const result = parseRepoURI(uri)
|
||||
const result = parseRepoGitURI(uri)
|
||||
return {
|
||||
repo: result.repoName,
|
||||
commit: result.revision ?? '',
|
||||
|
||||
@ -3,7 +3,7 @@ import { concat, from, of, Subscription, type Unsubscribable } from 'rxjs'
|
||||
import { first } from 'rxjs/operators'
|
||||
|
||||
import type { ActionContributionClientCommandUpdateConfiguration, Evaluated, KeyPath } from '@sourcegraph/client-api'
|
||||
import { formatSearchParameters } from '@sourcegraph/common'
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
import type { Position } from '@sourcegraph/extension-api-types'
|
||||
|
||||
import { wrapRemoteObservable } from '../api/client/api/common'
|
||||
@ -137,13 +137,7 @@ export function registerBuiltinClientCommands(
|
||||
* @param urlHash The current URL hash (beginning with '#' if non-empty).
|
||||
*/
|
||||
export function urlForOpenPanel(viewID: string, urlHash: string): string {
|
||||
// Preserve the existing URL fragment, if any.
|
||||
const parameters = new URLSearchParams(urlHash.slice('#'.length))
|
||||
parameters.set('tab', viewID)
|
||||
// In the URL fragment, the 'L1:2-3:4' is treated as a parameter with no value. Undo the escaping of ':'
|
||||
// and the addition of the '=' for the empty value, for aesthetic reasons.
|
||||
const parametersString = formatSearchParameters(parameters)
|
||||
return `#${parametersString}`
|
||||
return SourcegraphURL.from({ hash: urlHash }).setViewState(viewID).toString()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -51,7 +51,7 @@ import { languageSpecs } from '../codeintel/legacy-extensions/language-specs/lan
|
||||
import { getContributedActionItems } from '../contributions/contributions'
|
||||
import type { Controller, ExtensionsControllerProps } from '../extensions/controller'
|
||||
import type { PlatformContext, PlatformContextProps, URLToFileContext } from '../platform/context'
|
||||
import { makeRepoURI, parseRepoURI, withWorkspaceRootInputRevision } from '../util/url'
|
||||
import { makeRepoGitURI, parseRepoGitURI, withWorkspaceRootInputRevision } from '../util/url'
|
||||
|
||||
import type { HoverContext } from './HoverOverlay'
|
||||
|
||||
@ -147,7 +147,7 @@ export function getHoverActionsContext(
|
||||
hoverContext: HoveredToken & HoverContext
|
||||
): Observable<Context<TextDocumentPositionParameters>> {
|
||||
const parameters: TextDocumentPositionParameters & URLToFileContext = {
|
||||
textDocument: { uri: makeRepoURI(hoverContext) },
|
||||
textDocument: { uri: makeRepoGitURI(hoverContext) },
|
||||
position: { line: hoverContext.line - 1, character: hoverContext.character - 1 },
|
||||
part: hoverContext.part,
|
||||
}
|
||||
@ -267,7 +267,7 @@ export const getDefinitionURL =
|
||||
// Open the panel to show all definitions.
|
||||
const uri = withWorkspaceRootInputRevision(
|
||||
workspaceRoots || [],
|
||||
parseRepoURI(parameters.textDocument.uri)
|
||||
parseRepoGitURI(parameters.textDocument.uri)
|
||||
)
|
||||
return of({
|
||||
isLoading,
|
||||
@ -294,7 +294,7 @@ export const getDefinitionURL =
|
||||
// Preserve the input revision (e.g., a Git branch name instead of a Git commit SHA) if the result is
|
||||
// inside one of the current roots. This avoids navigating the user from (e.g.) a URL with a nice Git
|
||||
// branch name to a URL with a full Git commit SHA.
|
||||
const uri = withWorkspaceRootInputRevision(workspaceRoots || [], parseRepoURI(defer.uri))
|
||||
const uri = withWorkspaceRootInputRevision(workspaceRoots || [], parseRepoGitURI(defer.uri))
|
||||
if (defer.range) {
|
||||
uri.position = {
|
||||
line: defer.range.start.line + 1,
|
||||
|
||||
@ -6,9 +6,8 @@ import { SearchPatternType } from '../graphql-operations'
|
||||
|
||||
import {
|
||||
buildSearchURLQuery,
|
||||
makeRepoURI,
|
||||
parseHash,
|
||||
parseRepoURI,
|
||||
makeRepoGitURI,
|
||||
parseRepoGitURI,
|
||||
toPrettyBlobURL,
|
||||
withWorkspaceRootInputRevision,
|
||||
toAbsoluteBlobURL,
|
||||
@ -27,29 +26,29 @@ function assertDeepStrictEqual(actual: any, expected: any): void {
|
||||
expect(actual).toEqual(expected)
|
||||
}
|
||||
|
||||
describe('parseRepoURI', () => {
|
||||
describe('parseRepoGitURI', () => {
|
||||
test('should parse repo', () => {
|
||||
const parsed = parseRepoURI('git://github.com/gorilla/mux')
|
||||
const parsed = parseRepoGitURI('git://github.com/gorilla/mux')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
})
|
||||
})
|
||||
test('should parse repo with spaces', () => {
|
||||
const parsed = parseRepoURI('git://sourcegraph.visualstudio.com/Test%20Repo')
|
||||
const parsed = parseRepoGitURI('git://sourcegraph.visualstudio.com/Test%20Repo')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'sourcegraph.visualstudio.com/Test Repo',
|
||||
})
|
||||
})
|
||||
|
||||
test('should parse repo with plus sign', () => {
|
||||
const parsed = parseRepoURI('git://git.launchpad.net/ubuntu/+source/qemu')
|
||||
const parsed = parseRepoGitURI('git://git.launchpad.net/ubuntu/+source/qemu')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'git.launchpad.net/ubuntu/+source/qemu',
|
||||
})
|
||||
})
|
||||
|
||||
test('should parse repo with revision', () => {
|
||||
const parsed = parseRepoURI('git://github.com/gorilla/mux?branch')
|
||||
const parsed = parseRepoGitURI('git://github.com/gorilla/mux?branch')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
@ -57,7 +56,7 @@ describe('parseRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should parse repo with revision with special characters', () => {
|
||||
const parsed = parseRepoURI('git://github.com/gorilla/mux?my%2Fbranch')
|
||||
const parsed = parseRepoGitURI('git://github.com/gorilla/mux?my%2Fbranch')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'my/branch',
|
||||
@ -65,7 +64,7 @@ describe('parseRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should parse repo with commitID', () => {
|
||||
const parsed = parseRepoURI('git://github.com/gorilla/mux?24fca303ac6da784b9e8269f724ddeb0b2eea5e7')
|
||||
const parsed = parseRepoGitURI('git://github.com/gorilla/mux?24fca303ac6da784b9e8269f724ddeb0b2eea5e7')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: '24fca303ac6da784b9e8269f724ddeb0b2eea5e7',
|
||||
@ -74,7 +73,7 @@ describe('parseRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should parse repo with revision and file', () => {
|
||||
const parsed = parseRepoURI('git://github.com/gorilla/mux?branch#mux.go')
|
||||
const parsed = parseRepoGitURI('git://github.com/gorilla/mux?branch#mux.go')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
@ -83,7 +82,7 @@ describe('parseRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should parse repo with revision and file with spaces', () => {
|
||||
const parsed = parseRepoURI('git://github.com/gorilla/mux?branch#my%20file.go')
|
||||
const parsed = parseRepoGitURI('git://github.com/gorilla/mux?branch#my%20file.go')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
@ -92,7 +91,7 @@ describe('parseRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should parse repo with revision and file and line', () => {
|
||||
const parsed = parseRepoURI('git://github.com/gorilla/mux?branch#mux.go:3')
|
||||
const parsed = parseRepoGitURI('git://github.com/gorilla/mux?branch#mux.go:3')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
@ -105,7 +104,7 @@ describe('parseRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should parse repo with revision and file and position', () => {
|
||||
const parsed = parseRepoURI('git://github.com/gorilla/mux?branch#mux.go:3,5')
|
||||
const parsed = parseRepoGitURI('git://github.com/gorilla/mux?branch#mux.go:3,5')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
@ -118,7 +117,7 @@ describe('parseRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should parse repo with revision and file and range', () => {
|
||||
const parsed = parseRepoURI('git://github.com/gorilla/mux?branch#mux.go:3,5-6,9')
|
||||
const parsed = parseRepoGitURI('git://github.com/gorilla/mux?branch#mux.go:3,5-6,9')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
@ -137,7 +136,7 @@ describe('parseRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should parse a file with spaces', () => {
|
||||
const parsed = parseRepoURI('git://github.com/gorilla/mux?branch#space%20here.go')
|
||||
const parsed = parseRepoGitURI('git://github.com/gorilla/mux?branch#space%20here.go')
|
||||
assertDeepStrictEqual(parsed, {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
@ -146,16 +145,16 @@ describe('parseRepoURI', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeRepoURI', () => {
|
||||
describe('makeRepoGitURI', () => {
|
||||
test('should make repo', () => {
|
||||
const uri = makeRepoURI({
|
||||
const uri = makeRepoGitURI({
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
})
|
||||
assertDeepStrictEqual(uri, 'git://github.com/gorilla/mux')
|
||||
})
|
||||
|
||||
test('should make repo with revision', () => {
|
||||
const uri = makeRepoURI({
|
||||
const uri = makeRepoGitURI({
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
})
|
||||
@ -163,7 +162,7 @@ describe('makeRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should make repo with commitID', () => {
|
||||
const uri = makeRepoURI({
|
||||
const uri = makeRepoGitURI({
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
commitID: '24fca303ac6da784b9e8269f724ddeb0b2eea5e7',
|
||||
@ -172,7 +171,7 @@ describe('makeRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should make repo with revision and file', () => {
|
||||
const uri = makeRepoURI({
|
||||
const uri = makeRepoGitURI({
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
filePath: 'mux.go',
|
||||
@ -181,7 +180,7 @@ describe('makeRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should make repo with revision and file and line', () => {
|
||||
const uri = makeRepoURI({
|
||||
const uri = makeRepoGitURI({
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
filePath: 'mux.go',
|
||||
@ -194,7 +193,7 @@ describe('makeRepoURI', () => {
|
||||
})
|
||||
|
||||
test('should make repo with revision and file and position', () => {
|
||||
const uri = makeRepoURI({
|
||||
const uri = makeRepoGitURI({
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: 'branch',
|
||||
filePath: 'mux.go',
|
||||
@ -208,9 +207,7 @@ describe('makeRepoURI', () => {
|
||||
})
|
||||
|
||||
describe('util/url', () => {
|
||||
const linePosition = { line: 1 }
|
||||
const lineCharPosition = { line: 1, character: 1 }
|
||||
const referenceMode = { ...lineCharPosition, viewState: 'references' }
|
||||
const context: RepoFile = {
|
||||
repoName: 'github.com/gorilla/mux',
|
||||
revision: '',
|
||||
@ -218,57 +215,6 @@ describe('util/url', () => {
|
||||
filePath: 'mux.go',
|
||||
}
|
||||
|
||||
describe('parseHash', () => {
|
||||
test('parses empty hash', () => {
|
||||
expect(parseHash('')).toEqual({})
|
||||
})
|
||||
|
||||
test('parses unexpectedly formatted hash', () => {
|
||||
expect(parseHash('L-53')).toEqual({})
|
||||
expect(parseHash('L53:')).toEqual({})
|
||||
expect(parseHash('L1:2-')).toEqual({})
|
||||
expect(parseHash('L1:2-3')).toEqual({})
|
||||
expect(parseHash('L1:2-3:')).toEqual({})
|
||||
expect(parseHash('L1:-3:')).toEqual({})
|
||||
expect(parseHash('L1:-3:4')).toEqual({})
|
||||
expect(parseHash('L1-2:3')).toEqual({})
|
||||
expect(parseHash('L1-2:')).toEqual({})
|
||||
expect(parseHash('L1:-2')).toEqual({})
|
||||
expect(parseHash('L1:2--3:4')).toEqual({})
|
||||
expect(parseHash('L53:a')).toEqual({})
|
||||
})
|
||||
|
||||
test('parses hash with leading octothorpe', () => {
|
||||
expect(parseHash('#L1')).toEqual(linePosition)
|
||||
})
|
||||
|
||||
test('parses hash with line', () => {
|
||||
expect(parseHash('L1')).toEqual(linePosition)
|
||||
})
|
||||
|
||||
test('parses hash with line and character', () => {
|
||||
expect(parseHash('L1:1')).toEqual(lineCharPosition)
|
||||
})
|
||||
|
||||
test('parses hash with range', () => {
|
||||
expect(parseHash('L1-2')).toEqual({ line: 1, endLine: 2 })
|
||||
expect(parseHash('L1:2-3:4')).toEqual({ line: 1, character: 2, endLine: 3, endCharacter: 4 })
|
||||
expect(parseHash('L47-L55')).toEqual({ line: 47, endLine: 55 })
|
||||
expect(parseHash('L34:2-L38:3')).toEqual({ line: 34, character: 2, endLine: 38, endCharacter: 3 })
|
||||
})
|
||||
|
||||
test('parses hash with references', () => {
|
||||
expect(parseHash('$references')).toEqual({ viewState: 'references' })
|
||||
expect(parseHash('L1:1$references')).toEqual(referenceMode)
|
||||
expect(parseHash('L1:1$references')).toEqual(referenceMode)
|
||||
})
|
||||
test('parses modern hash with references', () => {
|
||||
expect(parseHash('tab=references')).toEqual({ viewState: 'references' })
|
||||
expect(parseHash('L1:1&tab=references')).toEqual(referenceMode)
|
||||
expect(parseHash('L1:1&tab=references')).toEqual(referenceMode)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toPrettyBlobURL', () => {
|
||||
test('formats url for empty revision', () => {
|
||||
expect(toPrettyBlobURL(context)).toBe('/github.com/gorilla/mux/-/blob/mux.go')
|
||||
@ -331,31 +277,31 @@ describe('util/url', () => {
|
||||
describe('withWorkspaceRootInputRevision', () => {
|
||||
test('uses input revision for URI inside root with input revision', () =>
|
||||
expect(
|
||||
withWorkspaceRootInputRevision([{ uri: 'git://r?c', inputRevision: 'v' }], parseRepoURI('git://r?c#f'))
|
||||
).toEqual(parseRepoURI('git://r?v#f')))
|
||||
withWorkspaceRootInputRevision([{ uri: 'git://r?c', inputRevision: 'v' }], parseRepoGitURI('git://r?c#f'))
|
||||
).toEqual(parseRepoGitURI('git://r?v#f')))
|
||||
|
||||
test('does not change URI outside root (different repoName)', () =>
|
||||
expect(
|
||||
withWorkspaceRootInputRevision([{ uri: 'git://r?c', inputRevision: 'v' }], parseRepoURI('git://r2?c#f'))
|
||||
).toEqual(parseRepoURI('git://r2?c#f')))
|
||||
withWorkspaceRootInputRevision([{ uri: 'git://r?c', inputRevision: 'v' }], parseRepoGitURI('git://r2?c#f'))
|
||||
).toEqual(parseRepoGitURI('git://r2?c#f')))
|
||||
|
||||
test('does not change URI outside root (different revision)', () =>
|
||||
expect(
|
||||
withWorkspaceRootInputRevision([{ uri: 'git://r?c', inputRevision: 'v' }], parseRepoURI('git://r?c2#f'))
|
||||
).toEqual(parseRepoURI('git://r?c2#f')))
|
||||
withWorkspaceRootInputRevision([{ uri: 'git://r?c', inputRevision: 'v' }], parseRepoGitURI('git://r?c2#f'))
|
||||
).toEqual(parseRepoGitURI('git://r?c2#f')))
|
||||
|
||||
test('uses empty string input revision (treats differently from undefined)', () =>
|
||||
expect(
|
||||
withWorkspaceRootInputRevision([{ uri: 'git://r?c', inputRevision: '' }], parseRepoURI('git://r?c#f'))
|
||||
).toEqual({ ...parseRepoURI('git://r?c#f'), revision: '' }))
|
||||
withWorkspaceRootInputRevision([{ uri: 'git://r?c', inputRevision: '' }], parseRepoGitURI('git://r?c#f'))
|
||||
).toEqual({ ...parseRepoGitURI('git://r?c#f'), revision: '' }))
|
||||
|
||||
test('does not change URI if root has undefined input revision', () =>
|
||||
expect(
|
||||
withWorkspaceRootInputRevision(
|
||||
[{ uri: 'git://r?c', inputRevision: undefined }],
|
||||
parseRepoURI('git://r?c#f')
|
||||
parseRepoGitURI('git://r?c#f')
|
||||
)
|
||||
).toEqual(parseRepoURI('git://r?c#f')))
|
||||
).toEqual(parseRepoGitURI('git://r?c#f')))
|
||||
})
|
||||
|
||||
describe('buildSearchURLQuery', () => {
|
||||
|
||||
@ -1,15 +1,6 @@
|
||||
import { parseURL } from 'whatwg-url'
|
||||
|
||||
import {
|
||||
addLineRangeQueryParameter,
|
||||
encodeURIPathComponent,
|
||||
escapeRevspecForURL,
|
||||
findLineKeyInSearchParameters,
|
||||
formatSearchParameters,
|
||||
type LineOrPositionOrRange,
|
||||
toPositionOrRangeQueryParameter,
|
||||
toViewStateHash,
|
||||
} from '@sourcegraph/common'
|
||||
import { encodeURIPathComponent, escapeRevspecForURL, SourcegraphURL } from '@sourcegraph/common'
|
||||
import type { Position } from '@sourcegraph/extension-api-types'
|
||||
|
||||
import type { WorkspaceRootWithMetadata } from '../api/extension/extensionHostApi'
|
||||
@ -178,7 +169,7 @@ const parsePosition = (string: string): Position => {
|
||||
* information to fetch the contents of.
|
||||
* @deprecated Migrate to using URLs to the Sourcegraph raw API (or other concrete URLs) instead.
|
||||
*/
|
||||
export function parseRepoURI(uri: RepoURI): ParsedRepoURI {
|
||||
export function parseRepoGitURI(uri: RepoURI): ParsedRepoURI {
|
||||
// We are not using the environments URL constructor because Chrome and Firefox do
|
||||
// not correctly parse out the hostname for URLs . We have a polyfill for the main web app
|
||||
// (see client/shared/src/polyfills/configure-core-js.ts) but that might not be used in all apps.
|
||||
@ -223,6 +214,21 @@ export function parseRepoURI(uri: RepoURI): ParsedRepoURI {
|
||||
return { repoName, revision, commitID, filePath: filePath || undefined, position, range }
|
||||
}
|
||||
|
||||
/**
|
||||
* The inverse of parseRepoGitURI, this generates a string from parsed values.
|
||||
* Example output: `git://github.com/gorilla/mux?SHA#mux.go:3,5-4,9`
|
||||
*/
|
||||
export function makeRepoGitURI(parsed: ParsedRepoURI): RepoURI {
|
||||
const revision = parsed.commitID || parsed.revision
|
||||
let uri = `git://${encodeURIPathComponent(parsed.repoName)}`
|
||||
uri += revision ? '?' + encodeURIPathComponent(revision) : ''
|
||||
uri += parsed.filePath ? '#' + encodeURIPathComponent(parsed.filePath) : ''
|
||||
uri += parsed.position || parsed.range ? ':' : ''
|
||||
uri += parsed.position ? positionString(parsed.position) : ''
|
||||
uri += parsed.range ? positionString(parsed.range.start) + '-' + positionString(parsed.range.end) : ''
|
||||
return uri
|
||||
}
|
||||
|
||||
/**
|
||||
* A repo
|
||||
*/
|
||||
@ -260,163 +266,6 @@ export interface AbsoluteRepoFilePosition
|
||||
Partial<ViewStateSpec>,
|
||||
Partial<RenderModeSpec> {}
|
||||
|
||||
/**
|
||||
* Tells if the given fragment component is a legacy blob hash component or not.
|
||||
* @param hash The URL fragment.
|
||||
*/
|
||||
export function isLegacyFragment(hash: string): boolean {
|
||||
if (hash.startsWith('#')) {
|
||||
hash = hash.slice('#'.length)
|
||||
}
|
||||
return (
|
||||
hash !== '' &&
|
||||
!hash.includes('=') &&
|
||||
(hash.includes('$info') ||
|
||||
hash.includes('$def') ||
|
||||
hash.includes('$references') ||
|
||||
hash.includes('$impl') ||
|
||||
hash.includes('$history'))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the URL search (query) portion and looks for a parameter which matches a line, position, or range in the file. If not found, it
|
||||
* falls back to parsing the hash for backwards compatibility.
|
||||
* @template V The type that describes the view state (typically a union of string constants). There is no runtime check that the return value satisfies V.
|
||||
*/
|
||||
export function parseQueryAndHash<V extends string>(
|
||||
query: string,
|
||||
hash: string
|
||||
): LineOrPositionOrRange & { viewState?: V } {
|
||||
const lpr = findLineInSearchParameters(new URLSearchParams(query))
|
||||
const parsedHash = parseHash<V>(hash)
|
||||
if (!lpr) {
|
||||
return parsedHash
|
||||
}
|
||||
return { ...lpr, viewState: parsedHash.viewState }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the URL fragment (hash) portion, which consists of a line, position, or range in the file, plus an
|
||||
* optional "viewState" parameter (that encodes other view state, such as for the panel).
|
||||
*
|
||||
* For example, in the URL fragment "#L17:19-21:23$foo:bar", the "viewState" is "foo:bar".
|
||||
* @template V The type that describes the view state (typically a union of string constants). There is no runtime check that the return value satisfies V.
|
||||
*/
|
||||
export function parseHash<V extends string>(hash: string): LineOrPositionOrRange & { viewState?: V } {
|
||||
if (hash.startsWith('#')) {
|
||||
hash = hash.slice('#'.length)
|
||||
}
|
||||
|
||||
if (!isLegacyFragment(hash)) {
|
||||
// Modern hash parsing logic (e.g. for hashes like `"#L17:19-21:23&tab=foo:bar"`:
|
||||
const searchParameters = new URLSearchParams(hash)
|
||||
const lpr = (findLineInSearchParameters(searchParameters) || {}) as LineOrPositionOrRange & {
|
||||
viewState?: V
|
||||
}
|
||||
if (searchParameters.get('tab')) {
|
||||
lpr.viewState = searchParameters.get('tab') as V
|
||||
}
|
||||
return lpr
|
||||
}
|
||||
|
||||
// Legacy hash parsing logic (e.g. for hashes like "#L17:19-21:23$foo:bar" where the "viewState" is "foo:bar"):
|
||||
if (!/^(L\d+(:\d+)?(-\d+(:\d+)?)?)?(\$.*)?$/.test(hash)) {
|
||||
// invalid or empty hash
|
||||
return {}
|
||||
}
|
||||
const lineCharModalInfo = hash.split('$', 2) // e.g. "L17:19-21:23$references"
|
||||
const lpr = parseLineOrPositionOrRange(lineCharModalInfo[0]) as LineOrPositionOrRange & { viewState?: V }
|
||||
if (lineCharModalInfo[1]) {
|
||||
lpr.viewState = lineCharModalInfo[1] as V
|
||||
}
|
||||
return lpr
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string like "L1-2:3", a range from a line to a position.
|
||||
*/
|
||||
function parseLineOrPositionOrRange(lineChar: string): LineOrPositionOrRange {
|
||||
if (!/^(L\d+(:\d+)?(-L?\d+(:\d+)?)?)?$/.test(lineChar)) {
|
||||
return {} // invalid
|
||||
}
|
||||
|
||||
// Parse the line or position range, ensuring we don't get an inconsistent result
|
||||
// (such as L1-2:3, a range from a line to a position).
|
||||
let line: number | undefined // 17
|
||||
let character: number | undefined // 19
|
||||
let endLine: number | undefined // 21
|
||||
let endCharacter: number | undefined // 23
|
||||
if (lineChar.startsWith('L')) {
|
||||
const positionOrRangeString = lineChar.slice(1)
|
||||
const [startString, endString] = positionOrRangeString.split('-', 2)
|
||||
if (startString) {
|
||||
const parsed = parseLineOrPosition(startString)
|
||||
line = parsed.line
|
||||
character = parsed.character
|
||||
}
|
||||
if (endString) {
|
||||
const parsed = parseLineOrPosition(endString)
|
||||
endLine = parsed.line
|
||||
endCharacter = parsed.character
|
||||
}
|
||||
}
|
||||
let lpr = { line, character, endLine, endCharacter } as LineOrPositionOrRange
|
||||
if (line === undefined || (endLine !== undefined && typeof character !== typeof endCharacter)) {
|
||||
lpr = {}
|
||||
} else if (character === undefined) {
|
||||
lpr = endLine === undefined ? { line } : { line, endLine }
|
||||
} else if (endLine === undefined || endCharacter === undefined) {
|
||||
lpr = { line, character }
|
||||
} else {
|
||||
lpr = { line, character, endLine, endCharacter }
|
||||
}
|
||||
return lpr
|
||||
}
|
||||
|
||||
function addRenderModeQueryParameter(
|
||||
searchParameters: URLSearchParams,
|
||||
context: Partial<RenderModeSpec>
|
||||
): URLSearchParams {
|
||||
if (context.renderMode === 'code') {
|
||||
searchParameters.set('view', 'code')
|
||||
}
|
||||
return searchParameters
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the URL search parameter which has a key like "L1-2:3" without any
|
||||
* value.
|
||||
* @param searchParameters The URLSearchParams to look for the line in.
|
||||
*/
|
||||
function findLineInSearchParameters(searchParameters: URLSearchParams): LineOrPositionOrRange | undefined {
|
||||
const key = findLineKeyInSearchParameters(searchParameters)
|
||||
return key ? parseLineOrPositionOrRange(key) : undefined
|
||||
}
|
||||
|
||||
function parseLineOrPosition(
|
||||
string: string
|
||||
): { line: undefined; character: undefined } | { line: number; character?: number } {
|
||||
if (string.startsWith('L')) {
|
||||
string = string.slice(1)
|
||||
}
|
||||
const parts = string.split(':', 2)
|
||||
let line: number | undefined
|
||||
let character: number | undefined
|
||||
if (parts.length >= 1) {
|
||||
line = parseInt(parts[0], 10)
|
||||
}
|
||||
if (parts.length === 2) {
|
||||
character = parseInt(parts[1], 10)
|
||||
}
|
||||
line = typeof line === 'number' && isNaN(line) ? undefined : line
|
||||
character = typeof character === 'number' && isNaN(character) ? undefined : character
|
||||
if (line === undefined) {
|
||||
return { line: undefined, character: undefined }
|
||||
}
|
||||
return { line, character }
|
||||
}
|
||||
|
||||
/** Encodes a repository at a revspec for use in a URL. */
|
||||
export function encodeRepoRevision({ repoName, revision }: RepoSpec & Partial<RevisionSpec>): string {
|
||||
return revision ? `${encodeURIPathComponent(repoName)}@${escapeRevspecForURL(revision)}` : repoName
|
||||
@ -432,15 +281,27 @@ export function toPrettyBlobURL(
|
||||
Partial<UIRangeSpec> &
|
||||
Partial<RenderModeSpec>
|
||||
): string {
|
||||
const searchParameters = addLineRangeQueryParameter(
|
||||
addRenderModeQueryParameter(new URLSearchParams(), target),
|
||||
toPositionOrRangeQueryParameter(target)
|
||||
)
|
||||
const searchQuery = [...searchParameters].length > 0 ? `?${formatSearchParameters(searchParameters)}` : ''
|
||||
return `/${encodeRepoRevision({
|
||||
repoName: target.repoName,
|
||||
revision: target.revision,
|
||||
})}/-/blob/${encodeURIPathComponent(target.filePath)}${searchQuery}${toViewStateHash(target.viewState)}`
|
||||
const url = SourcegraphURL.from({
|
||||
pathname: `${toRepoURL(target)}/-/blob/${encodeURIPathComponent(target.filePath)}`,
|
||||
})
|
||||
.setLineRange(
|
||||
target.range
|
||||
? {
|
||||
line: target.range.start.line,
|
||||
character: target.range.start.character,
|
||||
endLine: target.range.end.line,
|
||||
endCharacter: target.range.end.character,
|
||||
}
|
||||
: target.position
|
||||
? { line: target.position.line, character: target.position.character }
|
||||
: null
|
||||
)
|
||||
.setViewState(target.viewState)
|
||||
|
||||
if (target.renderMode === 'code') {
|
||||
url.setSearchParameter('view', 'code')
|
||||
}
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -470,27 +331,6 @@ export function toRepoURL(target: RepoSpec & Partial<RevisionSpec>): string {
|
||||
const positionString = (position: Position): string =>
|
||||
position.line.toString() + (position.character ? `,${position.character}` : '')
|
||||
|
||||
/**
|
||||
* The inverse of parseRepoURI, this generates a string from parsed values.
|
||||
*/
|
||||
export function makeRepoURI(parsed: ParsedRepoURI): RepoURI {
|
||||
const revision = parsed.commitID || parsed.revision
|
||||
let uri = `git://${encodeURIPathComponent(parsed.repoName)}`
|
||||
uri += revision ? '?' + encodeURIPathComponent(revision) : ''
|
||||
uri += parsed.filePath ? '#' + encodeURIPathComponent(parsed.filePath) : ''
|
||||
uri += parsed.position || parsed.range ? ':' : ''
|
||||
uri += parsed.position ? positionString(parsed.position) : ''
|
||||
uri += parsed.range ? positionString(parsed.range.start) + '-' + positionString(parsed.range.end) : ''
|
||||
return uri
|
||||
}
|
||||
|
||||
export const toRootURI = ({ repoName, commitID }: RepoSpec & ResolvedRevisionSpec): string =>
|
||||
`git://${encodeURIPathComponent(repoName)}?${commitID}`
|
||||
|
||||
export function toURIWithPath({ repoName, filePath, commitID }: RepoSpec & ResolvedRevisionSpec & FileSpec): string {
|
||||
return `git://${encodeURIPathComponent(repoName)}?${commitID}#${encodeURIPathComponent(filePath)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a URI to use the input revision (e.g., branch names) instead of the Git commit SHA if the URI is
|
||||
* inside of a workspace root. This helper is used to translate URLs (from actions such as go-to-definition) to
|
||||
@ -504,7 +344,7 @@ export function withWorkspaceRootInputRevision(
|
||||
uri: ParsedRepoURI
|
||||
): ParsedRepoURI {
|
||||
const inWorkspaceRoot = workspaceRoots.find(root => {
|
||||
const rootURI = parseRepoURI(root.uri)
|
||||
const rootURI = parseRepoGitURI(root.uri)
|
||||
return rootURI.repoName === uri.repoName && rootURI.revision === uri.revision
|
||||
})
|
||||
if (inWorkspaceRoot?.inputRevision !== undefined) {
|
||||
|
||||
@ -4,7 +4,7 @@ import { first, switchMap } from 'rxjs/operators'
|
||||
import type * as vscode from 'vscode'
|
||||
|
||||
import { finallyReleaseProxy, wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
|
||||
import { makeRepoURI, parseRepoURI } from '@sourcegraph/shared/src/util/url'
|
||||
import { makeRepoGitURI, parseRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import type { SearchSidebarAPI } from '../contract'
|
||||
import type { SourcegraphFileSystemProvider } from '../file-system/SourcegraphFileSystemProvider'
|
||||
@ -21,7 +21,7 @@ export class SourcegraphDefinitionProvider implements vscode.DefinitionProvider
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.Definition | undefined> {
|
||||
const uri = this.fs.sourcegraphUri(document.uri)
|
||||
const extensionHostUri = makeRepoURI({
|
||||
const extensionHostUri = makeRepoGitURI({
|
||||
repoName: uri.repositoryName,
|
||||
revision: uri.revision,
|
||||
filePath: uri.path,
|
||||
@ -46,7 +46,7 @@ export class SourcegraphDefinitionProvider implements vscode.DefinitionProvider
|
||||
}
|
||||
|
||||
const locations = result.map(location => {
|
||||
const uri = parseRepoURI(location.uri)
|
||||
const uri = parseRepoGitURI(location.uri)
|
||||
|
||||
return this.fs.toVscodeLocation({
|
||||
resource: {
|
||||
|
||||
@ -4,7 +4,7 @@ import { first, switchMap } from 'rxjs/operators'
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
import { finallyReleaseProxy, wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
|
||||
import { makeRepoURI } from '@sourcegraph/shared/src/util/url'
|
||||
import { makeRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import type { SearchSidebarAPI } from '../contract'
|
||||
import type { SourcegraphFileSystemProvider } from '../file-system/SourcegraphFileSystemProvider'
|
||||
@ -21,7 +21,7 @@ export class SourcegraphHoverProvider implements vscode.HoverProvider {
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.Hover | undefined> {
|
||||
const uri = this.fs.sourcegraphUri(document.uri)
|
||||
const extensionHostUri = makeRepoURI({
|
||||
const extensionHostUri = makeRepoGitURI({
|
||||
repoName: uri.repositoryName,
|
||||
revision: uri.revision,
|
||||
filePath: uri.path,
|
||||
|
||||
@ -4,7 +4,7 @@ import { debounceTime, first, switchMap } from 'rxjs/operators'
|
||||
import type * as vscode from 'vscode'
|
||||
|
||||
import { finallyReleaseProxy, wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/common'
|
||||
import { makeRepoURI, parseRepoURI } from '@sourcegraph/shared/src/util/url'
|
||||
import { makeRepoGitURI, parseRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import type { SearchSidebarAPI } from '../contract'
|
||||
import type { SourcegraphFileSystemProvider } from '../file-system/SourcegraphFileSystemProvider'
|
||||
@ -22,7 +22,7 @@ export class SourcegraphReferenceProvider implements vscode.ReferenceProvider {
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.Location[] | undefined> {
|
||||
const uri = this.fs.sourcegraphUri(document.uri)
|
||||
const extensionHostUri = makeRepoURI({
|
||||
const extensionHostUri = makeRepoGitURI({
|
||||
repoName: uri.repositoryName,
|
||||
revision: uri.revision,
|
||||
filePath: uri.path,
|
||||
@ -51,7 +51,7 @@ export class SourcegraphReferenceProvider implements vscode.ReferenceProvider {
|
||||
|
||||
const locations = result.map(location => {
|
||||
// Create a sourcegraph URI from this git URI (so we need both fromGitURI and toGitURI.)`
|
||||
const uri = parseRepoURI(location.uri)
|
||||
const uri = parseRepoGitURI(location.uri)
|
||||
|
||||
return this.fs.toVscodeLocation({
|
||||
resource: {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type * as Comlink from 'comlink'
|
||||
import vscode from 'vscode'
|
||||
|
||||
import { makeRepoURI } from '@sourcegraph/shared/src/util/url'
|
||||
import { makeRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import type { SearchSidebarAPI } from '../contract'
|
||||
import type { SourcegraphFileSystemProvider } from '../file-system/SourcegraphFileSystemProvider'
|
||||
@ -49,7 +49,7 @@ export function initializeCodeIntel({
|
||||
const sourcegraphUri = fs.sourcegraphUri(editor.document.uri)
|
||||
const languageId = toSourcegraphLanguage(editor.document.languageId)
|
||||
|
||||
const extensionHostUri = makeRepoURI({
|
||||
const extensionHostUri = makeRepoGitURI({
|
||||
repoName: sourcegraphUri.repositoryName,
|
||||
revision: sourcegraphUri.revision,
|
||||
filePath: sourcegraphUri.path,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
import type { Position } from '@sourcegraph/extension-api-types'
|
||||
import { parseQueryAndHash, parseRepoRevision } from '@sourcegraph/shared/src/util/url'
|
||||
import { parseRepoRevision } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
export interface SourcegraphUriOptionals {
|
||||
revision?: string
|
||||
@ -204,11 +205,11 @@ export class SourcegraphUri {
|
||||
}
|
||||
let position: Position | undefined
|
||||
|
||||
const parsedHash = parseQueryAndHash(url.search, url.hash)
|
||||
if (parsedHash.line) {
|
||||
const lineRange = SourcegraphURL.from(url.toString()).lineRange
|
||||
if (lineRange.line) {
|
||||
position = {
|
||||
line: parsedHash.line,
|
||||
character: parsedHash.character || 0,
|
||||
line: lineRange.line,
|
||||
character: lineRange.character || 0,
|
||||
}
|
||||
}
|
||||
const isDirectory = uri.includes('/-/tree/')
|
||||
|
||||
@ -7,7 +7,7 @@ import { map } from 'rxjs/operators'
|
||||
|
||||
import type { HoverMerged } from '@sourcegraph/client-api'
|
||||
import type { Hoverifier } from '@sourcegraph/codeintellify'
|
||||
import { appendLineRangeQueryParameter, toPositionOrRangeQueryParameter } from '@sourcegraph/common'
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
import type { ActionItemAction } from '@sourcegraph/shared/src/actions/ActionItem'
|
||||
import type { FetchFileParameters } from '@sourcegraph/shared/src/backend/file'
|
||||
import type { MatchGroupMatch } from '@sourcegraph/shared/src/components/ranking/PerFileResultRanking'
|
||||
@ -198,10 +198,8 @@ export const FileMatchChildren: React.FunctionComponent<React.PropsWithChildren<
|
||||
[result, fetchHighlightedFileLineRanges, grouped, telemetryService]
|
||||
)
|
||||
|
||||
const createCodeExcerptLink = (group: MatchGroup): string => {
|
||||
const positionOrRangeQueryParameter = toPositionOrRangeQueryParameter({ position: group.position })
|
||||
return appendLineRangeQueryParameter(getFileMatchUrl(result), positionOrRangeQueryParameter)
|
||||
}
|
||||
const createCodeExcerptLink = (group: MatchGroup): string =>
|
||||
SourcegraphURL.from(getFileMatchUrl(result)).setLineRange(group.position).toString()
|
||||
|
||||
/**
|
||||
* This handler implements the logic to simulate the click/keyboard
|
||||
|
||||
@ -4,18 +4,13 @@ export type { ErrorLike } from '@sourcegraph/common/src/errors/types'
|
||||
export { isErrorLike } from '@sourcegraph/common/src/errors/utils'
|
||||
export { createAggregateError, asError } from '@sourcegraph/common/src/errors/errors'
|
||||
export { memoizeObservable, resetAllMemoizationCaches } from '@sourcegraph/common/src/util/rxjs/memoizeObservable'
|
||||
export {
|
||||
encodeURIPathComponent,
|
||||
toPositionOrRangeQueryParameter,
|
||||
addLineRangeQueryParameter,
|
||||
formatSearchParameters,
|
||||
} from '@sourcegraph/common/src/util/url'
|
||||
export { encodeURIPathComponent } from '@sourcegraph/common/src/util/url'
|
||||
export { pluralize, numberWithCommas } from '@sourcegraph/common/src/util/strings'
|
||||
export { renderMarkdown } from '@sourcegraph/common/src/util/markdown/markdown'
|
||||
export { highlightNodeMultiline, highlightNode } from '@sourcegraph/common/src/util/highlightNode'
|
||||
export { logger } from '@sourcegraph/common/src/util/logger'
|
||||
export { isSafari } from '@sourcegraph/common/src/util/browserDetection'
|
||||
export { isExternalLink, type LineOrPositionOrRange } from '@sourcegraph/common/src/util/url'
|
||||
export { isExternalLink, type LineOrPositionOrRange, SourcegraphURL } from '@sourcegraph/common/src/util/url'
|
||||
export { parseJSONCOrError } from '@sourcegraph/common/src/util/jsonc'
|
||||
export { isWindowsPlatform, isMacPlatform, isLinuxPlatform } from '@sourcegraph/common/src/util/browserDetection'
|
||||
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
import { describe, test, expect } from 'vitest'
|
||||
|
||||
import { updateSearchParamsWithLineInformation } from './blob'
|
||||
|
||||
describe('updateSearchParamsWithLineInformation', () => {
|
||||
test.each`
|
||||
range | expected
|
||||
${{ line: 5 }} | ${'L5'}
|
||||
${{ line: 5, character: 3 }} | ${'L5'}
|
||||
${{ line: 5, endLine: 7 }} | ${'L5-7'}
|
||||
`('$range -> $expected', ({ range, expected }) => {
|
||||
expect(updateSearchParamsWithLineInformation(new URLSearchParams(), range)).toBe(expected)
|
||||
})
|
||||
|
||||
test('replace existing line information', () => {
|
||||
expect(updateSearchParamsWithLineInformation(new URLSearchParams('L1'), { line: 2 })).toBe('L2')
|
||||
})
|
||||
|
||||
test('preserve other parameters', () => {
|
||||
expect(updateSearchParamsWithLineInformation(new URLSearchParams('existing=param'), { line: 2 })).toBe(
|
||||
'L2&existing=param'
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -5,12 +5,10 @@ import { get, type Readable, readable } from 'svelte/store'
|
||||
|
||||
import { goto as svelteGoto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { addLineRangeQueryParameter, formatSearchParameters, toPositionOrRangeQueryParameter } from '$lib/common'
|
||||
import {
|
||||
positionToOffset,
|
||||
type Definition,
|
||||
type GoToDefinitionOptions,
|
||||
type SelectedLineRange,
|
||||
showTemporaryTooltip,
|
||||
locationToURL,
|
||||
type DocumentInfo,
|
||||
@ -31,29 +29,6 @@ import type { BlobPage_Blob } from '../../routes/[...repo=reporev]/(validrev)/(c
|
||||
*/
|
||||
const MINIMUM_GO_TO_DEF_LATENCY_MILLIS = 20
|
||||
|
||||
export function updateSearchParamsWithLineInformation(
|
||||
currentSearchParams: URLSearchParams,
|
||||
range: SelectedLineRange
|
||||
): string {
|
||||
const parameters = new URLSearchParams(currentSearchParams)
|
||||
parameters.delete('popover')
|
||||
|
||||
let query: string | undefined
|
||||
|
||||
if (range?.line !== range?.endLine && range?.endLine) {
|
||||
query = toPositionOrRangeQueryParameter({
|
||||
range: {
|
||||
start: { line: range.line },
|
||||
end: { line: range.endLine },
|
||||
},
|
||||
})
|
||||
} else if (range?.line) {
|
||||
query = toPositionOrRangeQueryParameter({ position: { line: range.line } })
|
||||
}
|
||||
|
||||
return formatSearchParameters(addLineRangeQueryParameter(parameters, query))
|
||||
}
|
||||
|
||||
export async function goToDefinition(
|
||||
documentInfo: DocumentInfo,
|
||||
view: EditorView,
|
||||
|
||||
@ -2,12 +2,7 @@
|
||||
|
||||
export type { AbsoluteRepoFile } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
export {
|
||||
parseRepoRevision,
|
||||
parseQueryAndHash,
|
||||
buildSearchURLQuery,
|
||||
makeRepoURI,
|
||||
} from '@sourcegraph/shared/src/util/url'
|
||||
export { parseRepoRevision, buildSearchURLQuery, makeRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
export {
|
||||
isCloneInProgressErrorLike,
|
||||
|
||||
@ -7,15 +7,15 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import CodeMirrorBlob from '$lib/CodeMirrorBlob.svelte'
|
||||
import { isErrorLike, type LineOrPositionOrRange } from '$lib/common'
|
||||
import { isErrorLike, SourcegraphURL, type LineOrPositionOrRange } from '$lib/common'
|
||||
import { toGraphQLResult } from '$lib/graphql'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
|
||||
import { updateSearchParamsWithLineInformation, createBlobDataHandler } from '$lib/repo/blob'
|
||||
import { createBlobDataHandler } from '$lib/repo/blob'
|
||||
import FileDiff from '$lib/repo/FileDiff.svelte'
|
||||
import FileHeader from '$lib/repo/FileHeader.svelte'
|
||||
import Permalink from '$lib/repo/Permalink.svelte'
|
||||
import { createCodeIntelAPI, parseQueryAndHash } from '$lib/shared'
|
||||
import { createCodeIntelAPI } from '$lib/shared'
|
||||
import { Alert } from '$lib/wildcard'
|
||||
import markdownStyles from '$lib/wildcard/Markdown.module.scss'
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
})
|
||||
$: if (!blobPending) {
|
||||
// Update selected position as soon as blob is loaded
|
||||
selectedPosition = parseQueryAndHash($page.url.search, $page.url.hash)
|
||||
selectedPosition = SourcegraphURL.from($page.url).lineRange
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -108,8 +108,12 @@
|
||||
{highlights}
|
||||
wrapLines={$lineWrap}
|
||||
selectedLines={selectedPosition?.line ? selectedPosition : null}
|
||||
on:selectline={event => {
|
||||
goto('?' + updateSearchParamsWithLineInformation($page.url.searchParams, event.detail))
|
||||
on:selectline={({ detail: range }) => {
|
||||
goto(
|
||||
SourcegraphURL.from($page.url.searchParams)
|
||||
.setLineRange(range ? { line: range.line, endLine: range.endLine } : null)
|
||||
.deleteSearchParameter('popover').search
|
||||
)
|
||||
}}
|
||||
{codeIntelAPI}
|
||||
/>
|
||||
|
||||
@ -9,12 +9,7 @@
|
||||
<script lang="ts">
|
||||
import { mdiChevronDown, mdiChevronUp } from '@mdi/js'
|
||||
|
||||
import {
|
||||
addLineRangeQueryParameter,
|
||||
formatSearchParameters,
|
||||
pluralize,
|
||||
toPositionOrRangeQueryParameter,
|
||||
} from '$lib/common'
|
||||
import { pluralize, SourcegraphURL } from '$lib/common'
|
||||
import Icon from '$lib/Icon.svelte'
|
||||
import { observeIntersection } from '$lib/intersection-observer'
|
||||
import { fetchFileRangeMatches } from '$lib/search/api/highlighting'
|
||||
@ -61,16 +56,8 @@
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function getMatchURL(startLine: number, endLine: number): string {
|
||||
const searchParams = formatSearchParameters(
|
||||
addLineRangeQueryParameter(
|
||||
// We don't want to preserve the 'q' query parameter.
|
||||
// We might have to adjust this if we want to preserve other query parameters.
|
||||
new URLSearchParams(),
|
||||
toPositionOrRangeQueryParameter({ range: { start: { line: startLine }, end: { line: endLine } } })
|
||||
)
|
||||
)
|
||||
return `${fileURL}?${searchParams}`
|
||||
function getMatchURL(line: number, endLine: number): string {
|
||||
return SourcegraphURL.from(fileURL).setLineRange({ line, endLine }).toString()
|
||||
}
|
||||
|
||||
let visible = false
|
||||
|
||||
@ -9,7 +9,7 @@ import VisibilitySensor from 'react-visibility-sensor'
|
||||
import type { Observable } from 'rxjs'
|
||||
|
||||
import { CodeExcerpt } from '@sourcegraph/branded'
|
||||
import { type ErrorLike, logger, pluralize } from '@sourcegraph/common'
|
||||
import { type ErrorLike, logger, pluralize, SourcegraphURL } from '@sourcegraph/common'
|
||||
import { Position } from '@sourcegraph/extension-api-classes'
|
||||
import type { FetchFileParameters } from '@sourcegraph/shared/src/backend/file'
|
||||
import { displayRepoName } from '@sourcegraph/shared/src/components/RepoLink'
|
||||
@ -23,7 +23,6 @@ import {
|
||||
type RevisionSpec,
|
||||
type FileSpec,
|
||||
type ResolvedRevisionSpec,
|
||||
parseQueryAndHash,
|
||||
toPrettyBlobURL,
|
||||
} from '@sourcegraph/shared/src/util/url'
|
||||
import {
|
||||
@ -103,8 +102,11 @@ interface OneBasedPosition {
|
||||
}
|
||||
|
||||
function createStateFromLocation(location: H.Location): null | State {
|
||||
const { hash, pathname, search } = location
|
||||
const { line, character, endLine, endCharacter, viewState } = parseQueryAndHash(search, hash)
|
||||
const { pathname, search } = location
|
||||
const {
|
||||
lineRange: { line, character, endLine, endCharacter },
|
||||
viewState,
|
||||
} = SourcegraphURL.from(location)
|
||||
const { filePath, repoName, revision } = parseBrowserRepoURL(pathname)
|
||||
|
||||
// If we don't have enough information in the URL, we can't render the panel
|
||||
|
||||
@ -2,7 +2,7 @@ import { extname } from 'path'
|
||||
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
|
||||
import { appendLineRangeQueryParameter, toPositionOrRangeQueryParameter } from '@sourcegraph/common'
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
import type { Range } from '@sourcegraph/extension-api-types'
|
||||
|
||||
import { raceWithDelayOffset } from '../../codeintel/promise'
|
||||
@ -365,12 +365,12 @@ function lineMatchesToResults(
|
||||
{ lineNumber, offsetAndLengths }: LineMatch
|
||||
): Result[] {
|
||||
return offsetAndLengths.map(([offset, length]) => {
|
||||
const url = appendLineRangeQueryParameter(
|
||||
fileUrl,
|
||||
toPositionOrRangeQueryParameter({
|
||||
position: { line: lineNumber + 1, character: offset + 1 },
|
||||
const url = SourcegraphURL.from(fileUrl)
|
||||
.setLineRange({
|
||||
line: lineNumber + 1,
|
||||
character: offset + 1,
|
||||
})
|
||||
)
|
||||
.toString()
|
||||
return {
|
||||
repo,
|
||||
rev: revision,
|
||||
|
||||
@ -4,13 +4,8 @@ import { EditorState, type Extension } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
addLineRangeQueryParameter,
|
||||
formatSearchParameters,
|
||||
toPositionOrRangeQueryParameter,
|
||||
} from '@sourcegraph/common'
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
import { CodeMirrorEditor, defaultEditorTheme } from '@sourcegraph/shared/src/components/CodeMirrorEditor'
|
||||
import { parseQueryAndHash } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import { selectableLineNumbers } from '../../repo/blob/codemirror/linenumbers'
|
||||
|
||||
@ -26,12 +21,8 @@ const theme = EditorView.theme({
|
||||
export const IngestedFileViewer: React.FunctionComponent<{ contents: string }> = ({ contents }) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const search = location.search
|
||||
|
||||
const lineNumber = useMemo(
|
||||
() => parseQueryAndHash(location.search, location.hash).line,
|
||||
[location.search, location.hash]
|
||||
)
|
||||
const lineNumber = useMemo(() => SourcegraphURL.from(location).lineRange.line, [location])
|
||||
|
||||
const extensions: Extension[] = useMemo(
|
||||
() => [
|
||||
@ -39,21 +30,17 @@ export const IngestedFileViewer: React.FunctionComponent<{ contents: string }> =
|
||||
theme,
|
||||
selectableLineNumbers({
|
||||
onSelection(range) {
|
||||
let query
|
||||
if (range) {
|
||||
const position = { line: range.line }
|
||||
query = toPositionOrRangeQueryParameter(
|
||||
range.endLine ? { range: { start: position, end: { line: range.endLine } } } : { position }
|
||||
)
|
||||
}
|
||||
const newSearchParameters = addLineRangeQueryParameter(new URLSearchParams(search), query)
|
||||
navigate('?' + formatSearchParameters(newSearchParameters))
|
||||
navigate(
|
||||
SourcegraphURL.from({ search: location.search, hash: location.hash })
|
||||
.setLineRange(range ? { line: range.line, endLine: range?.endLine } : null)
|
||||
.toString()
|
||||
)
|
||||
},
|
||||
initialSelection: lineNumber ? { line: lineNumber } : null,
|
||||
}),
|
||||
defaultEditorTheme,
|
||||
],
|
||||
[lineNumber, search, navigate]
|
||||
[lineNumber, location, navigate]
|
||||
)
|
||||
|
||||
return <CodeMirrorEditor value={contents} extensions={extensions} />
|
||||
|
||||
@ -2,10 +2,10 @@ import type { FC } from 'react'
|
||||
|
||||
import { Navigate, useLocation } from 'react-router-dom'
|
||||
|
||||
import { appendLineRangeQueryParameter } from '@sourcegraph/common'
|
||||
import { SourcegraphURL, isLegacyFragment } from '@sourcegraph/common'
|
||||
import { TraceSpanProvider } from '@sourcegraph/observability-client'
|
||||
import { getModeFromPath } from '@sourcegraph/shared/src/languages'
|
||||
import { isLegacyFragment, parseQueryAndHash, toRepoURL } from '@sourcegraph/shared/src/util/url'
|
||||
import { toRepoURL } from '@sourcegraph/shared/src/util/url'
|
||||
import { LoadingSpinner } from '@sourcegraph/wildcard'
|
||||
|
||||
import { ErrorBoundary } from '../components/ErrorBoundary'
|
||||
@ -13,7 +13,7 @@ import type { SourcegraphContext } from '../jscontext'
|
||||
import type { NotebookProps } from '../notebooks'
|
||||
import type { OwnConfigProps } from '../own/OwnConfigProps'
|
||||
import { GettingStartedTour } from '../tour/GettingStartedTour'
|
||||
import { formatHash, formatLineOrPositionOrRange, parseBrowserRepoURL } from '../util/url'
|
||||
import { parseBrowserRepoURL } from '../util/url'
|
||||
|
||||
import { BlobPage } from './blob/BlobPage'
|
||||
import type { RepoRevisionContainerContext } from './RepoRevisionContainer'
|
||||
@ -61,24 +61,34 @@ export const RepositoryFileTreePage: FC<RepositoryFileTreePageProps> = props =>
|
||||
if (objectType === 'blob' && hashLineNumberMatch) {
|
||||
const startLineNumber = parseInt(hashLineNumberMatch[1], 10)
|
||||
const endLineNumber = hashLineNumberMatch[2] ? parseInt(hashLineNumberMatch[2].slice(1), 10) : undefined
|
||||
const url = appendLineRangeQueryParameter(
|
||||
location.pathname + location.search,
|
||||
`L${startLineNumber}` + (endLineNumber ? `-${endLineNumber}` : '')
|
||||
// Navigate's to doesn't seem to work with the SourcegraphURL object, so we need to convert it to a string
|
||||
return (
|
||||
<Navigate
|
||||
to={SourcegraphURL.from({ pathname: location.pathname, search: location.search })
|
||||
.setLineRange({
|
||||
line: startLineNumber,
|
||||
endLine: endLineNumber,
|
||||
})
|
||||
.toString()}
|
||||
replace={true}
|
||||
/>
|
||||
)
|
||||
return <Navigate to={url} replace={true} />
|
||||
}
|
||||
|
||||
// For blob pages with legacy URL fragment hashes like "#L17:19-21:23$foo:bar"
|
||||
// redirect to the modern URL fragment hashes like "#L17:19-21:23&tab=foo:bar"
|
||||
// redirect to the modern URL structure like "?L17:19-21:23#tab=foo:bar"
|
||||
if (!hideRepoRevisionContent && objectType === 'blob' && isLegacyFragment(location.hash)) {
|
||||
const parsedQuery = parseQueryAndHash(location.search, location.hash)
|
||||
const hashParameters = new URLSearchParams()
|
||||
if (parsedQuery.viewState) {
|
||||
hashParameters.set('tab', parsedQuery.viewState)
|
||||
}
|
||||
const range = formatLineOrPositionOrRange(parsedQuery)
|
||||
const url = appendLineRangeQueryParameter(location.pathname + location.search, range ? `L${range}` : undefined)
|
||||
return <Navigate to={url + formatHash(hashParameters)} replace={true} />
|
||||
const { lineRange, viewState } = SourcegraphURL.from(location)
|
||||
// Navigate's to doesn't seem to work with the SourcegraphURL object, so we need to convert it to a string
|
||||
return (
|
||||
<Navigate
|
||||
to={SourcegraphURL.from({ pathname: location.pathname, search: location.search })
|
||||
.setLineRange(lineRange)
|
||||
.setViewState(viewState)
|
||||
.toString()}
|
||||
replace={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
RevisionNotFoundError,
|
||||
} from '@sourcegraph/shared/src/backend/errors'
|
||||
import {
|
||||
makeRepoURI,
|
||||
makeRepoGitURI,
|
||||
type RepoRevision,
|
||||
type RepoSpec,
|
||||
type ResolvedRevisionSpec,
|
||||
@ -168,7 +168,7 @@ export const resolveRepoRevision = memoizeObservable(
|
||||
}
|
||||
})
|
||||
),
|
||||
makeRepoURI
|
||||
makeRepoGitURI
|
||||
)
|
||||
|
||||
export const fetchFileExternalLinks = memoizeObservable(
|
||||
@ -198,7 +198,7 @@ export const fetchFileExternalLinks = memoizeObservable(
|
||||
return data.repository.commit.file.externalURLs
|
||||
})
|
||||
),
|
||||
makeRepoURI
|
||||
makeRepoGitURI
|
||||
)
|
||||
|
||||
interface FetchCommitMessageResult {
|
||||
@ -227,5 +227,5 @@ export const fetchCommitMessage = memoizeObservable(
|
||||
map(dataOrThrowErrors),
|
||||
map(data => data.repository.commit.message)
|
||||
),
|
||||
makeRepoURI
|
||||
makeRepoGitURI
|
||||
)
|
||||
|
||||
@ -8,7 +8,7 @@ import { catchError, map, throttleTime } from 'rxjs/operators'
|
||||
|
||||
import { type ErrorLike, memoizeObservable } from '@sourcegraph/common'
|
||||
import { dataOrThrowErrors, gql } from '@sourcegraph/http-client'
|
||||
import { makeRepoURI } from '@sourcegraph/shared/src/util/url'
|
||||
import { makeRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
import { useObservable } from '@sourcegraph/wildcard'
|
||||
|
||||
import { requestGraphQL } from '../../backend/graphql'
|
||||
@ -188,7 +188,7 @@ const fetchBlameViaStreaming = memoizeObservable(
|
||||
throttleTime(1000, undefined, { leading: true, trailing: true }),
|
||||
catchError(error => of(error))
|
||||
),
|
||||
makeRepoURI
|
||||
makeRepoGitURI
|
||||
)
|
||||
|
||||
async function fetchRepositoryData(repoName: string): Promise<Omit<BlameHunkData, 'current'>> {
|
||||
|
||||
@ -13,7 +13,7 @@ import type { Optional } from 'utility-types'
|
||||
import type { StreamingSearchResultsListProps } from '@sourcegraph/branded'
|
||||
import { TabbedPanelContent } from '@sourcegraph/branded/src/components/panel/TabbedPanelContent'
|
||||
import { NoopEditor } from '@sourcegraph/cody-shared/dist/editor'
|
||||
import { asError, type ErrorLike, isErrorLike, basename } from '@sourcegraph/common'
|
||||
import { asError, type ErrorLike, isErrorLike, basename, SourcegraphURL } from '@sourcegraph/common'
|
||||
import {
|
||||
createActiveSpan,
|
||||
reactManualTracer,
|
||||
@ -28,7 +28,7 @@ import { type SettingsCascadeProps, useExperimentalFeatures } from '@sourcegraph
|
||||
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
|
||||
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
|
||||
import { type ModeSpec, parseQueryAndHash, type RepoFile } from '@sourcegraph/shared/src/util/url'
|
||||
import { type ModeSpec, type RepoFile } from '@sourcegraph/shared/src/util/url'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@ -143,10 +143,7 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
|
||||
const [enableOwnershipPanels] = useFeatureFlag('enable-ownership-panels', true)
|
||||
const enableOwnershipPanel = enableOwnershipPanels && props.ownEnabled
|
||||
|
||||
const lineOrRange = useMemo(
|
||||
() => parseQueryAndHash(location.search, location.hash),
|
||||
[location.search, location.hash]
|
||||
)
|
||||
const { lineRange: lineOrRange, viewState } = useMemo(() => SourcegraphURL.from(location), [location])
|
||||
|
||||
// Log view event whenever a new Blob, or a Blob with a different render mode, is visited.
|
||||
useEffect(() => {
|
||||
@ -618,7 +615,7 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
|
||||
/>
|
||||
</TraceSpanProvider>
|
||||
)}
|
||||
{parseQueryAndHash(location.search, location.hash).viewState &&
|
||||
{viewState &&
|
||||
createPortal(
|
||||
<Panel
|
||||
className={styles.panel}
|
||||
|
||||
@ -15,11 +15,7 @@ import { createRoot } from 'react-dom/client'
|
||||
import { createPath, useLocation, useNavigate, type Location, type NavigateFunction } from 'react-router-dom'
|
||||
|
||||
import { NoopEditor } from '@sourcegraph/cody-shared/dist/editor'
|
||||
import {
|
||||
addLineRangeQueryParameter,
|
||||
formatSearchParameters,
|
||||
toPositionOrRangeQueryParameter,
|
||||
} from '@sourcegraph/common'
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
import { createCodeIntelAPI } from '@sourcegraph/shared/src/codeintel/api'
|
||||
import { editorHeight, useCodeMirror, useCompartment } from '@sourcegraph/shared/src/components/CodeMirrorEditor'
|
||||
import { useKeyboardShortcut } from '@sourcegraph/shared/src/keyboardShortcuts/useKeyboardShortcut'
|
||||
@ -31,7 +27,6 @@ import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetry
|
||||
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
|
||||
import { codeCopiedEvent } from '@sourcegraph/shared/src/tracking/event-log-creators'
|
||||
import {
|
||||
parseQueryAndHash,
|
||||
toPrettyBlobURL,
|
||||
type AbsoluteRepoFile,
|
||||
type BlobViewState,
|
||||
@ -236,17 +231,14 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = props => {
|
||||
// This is used to avoid reinitializing the editor when new locations in the
|
||||
// same file are opened inside the reference panel.
|
||||
const blobInfo = useDistinctBlob(props.blobInfo)
|
||||
const position = useMemo(() => {
|
||||
const position = useMemo(
|
||||
// When an activeURL is passed, it takes presedence over the react
|
||||
// router location API.
|
||||
//
|
||||
// This is needed to support the reference panel
|
||||
if (props.activeURL) {
|
||||
const url = new URL(props.activeURL, window.location.href)
|
||||
return parseQueryAndHash(url.search, url.hash)
|
||||
}
|
||||
return parseQueryAndHash(location.search, location.hash)
|
||||
}, [props.activeURL, location.search, location.hash])
|
||||
() => SourcegraphURL.from(props.activeURL || location).lineRange,
|
||||
[props.activeURL, location]
|
||||
)
|
||||
const hasPin = useMemo(() => urlIsPinned(location.search), [location.search])
|
||||
|
||||
// Keep history and location in a ref so that we can use the latest value in
|
||||
@ -275,32 +267,20 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = props => {
|
||||
const customHistoryAction = props.nav
|
||||
const onSelection = useCallback(
|
||||
(range: SelectedLineRange) => {
|
||||
const parameters = new URLSearchParams(locationRef.current.search)
|
||||
parameters.delete('popover')
|
||||
const url = SourcegraphURL.from(locationRef.current)
|
||||
.deleteSearchParameter('popover')
|
||||
.setLineRange(range ? { line: range.line, endLine: range.endLine } : null)
|
||||
|
||||
let query: string | undefined
|
||||
|
||||
if (range?.line !== range?.endLine && range?.endLine) {
|
||||
query = toPositionOrRangeQueryParameter({
|
||||
range: {
|
||||
start: { line: range.line },
|
||||
end: { line: range.endLine },
|
||||
},
|
||||
})
|
||||
} else if (range?.line) {
|
||||
query = toPositionOrRangeQueryParameter({ position: { line: range.line } })
|
||||
}
|
||||
|
||||
const newSearchParameters = addLineRangeQueryParameter(parameters, query)
|
||||
if (customHistoryAction) {
|
||||
customHistoryAction(
|
||||
createPath({
|
||||
...locationRef.current,
|
||||
search: formatSearchParameters(newSearchParameters),
|
||||
pathname: url.pathname,
|
||||
search: url.search,
|
||||
hash: url.hash,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
updateBrowserHistoryIfChanged(navigate, locationRef.current, newSearchParameters)
|
||||
updateBrowserHistoryIfChanged(navigate, locationRef.current, url)
|
||||
}
|
||||
},
|
||||
[customHistoryAction, locationRef, navigate]
|
||||
@ -679,28 +659,21 @@ function useCodeIntelExtension(
|
||||
},
|
||||
pin: {
|
||||
onPin(position) {
|
||||
const search = new URLSearchParams(locationRef.current.search)
|
||||
search.set('popover', 'pinned')
|
||||
|
||||
updateBrowserHistoryIfChanged(
|
||||
navigate,
|
||||
locationRef.current,
|
||||
// It may seem strange to set start and end to the same value, but that what's the old blob view is doing as well
|
||||
addLineRangeQueryParameter(
|
||||
search,
|
||||
toPositionOrRangeQueryParameter({
|
||||
position,
|
||||
range: { start: position, end: position },
|
||||
})
|
||||
)
|
||||
SourcegraphURL.from(locationRef.current)
|
||||
.setSearchParameter('popover', 'pinned')
|
||||
.setLineRange(position)
|
||||
)
|
||||
void navigator.clipboard.writeText(window.location.href)
|
||||
},
|
||||
onUnpin() {
|
||||
const parameters = new URLSearchParams(locationRef.current.search)
|
||||
parameters.delete('popover')
|
||||
|
||||
updateBrowserHistoryIfChanged(navigate, locationRef.current, parameters)
|
||||
updateBrowserHistoryIfChanged(
|
||||
navigate,
|
||||
locationRef.current,
|
||||
SourcegraphURL.from(locationRef.current).deleteSearchParameter('popover')
|
||||
)
|
||||
},
|
||||
},
|
||||
navigate,
|
||||
@ -853,7 +826,7 @@ function useMutableValue<T>(value: T): Readonly<MutableRefObject<T>> {
|
||||
export function updateBrowserHistoryIfChanged(
|
||||
navigate: NavigateFunction,
|
||||
location: Location,
|
||||
newSearchParameters: URLSearchParams,
|
||||
newLocation: SourcegraphURL,
|
||||
/** If set to true replace the current history entry instead of adding a new one. */
|
||||
replace: boolean = false
|
||||
): void {
|
||||
@ -865,15 +838,10 @@ export function updateBrowserHistoryIfChanged(
|
||||
// non-existing key in the new search parameters and thus return `null`
|
||||
// (whereas it returns an empty string in the current search parameters).
|
||||
const needsUpdate =
|
||||
currentSearchParameters.length !== [...newSearchParameters.keys()].length ||
|
||||
currentSearchParameters.some(([key, value]) => newSearchParameters.get(key) !== value)
|
||||
currentSearchParameters.length !== [...newLocation.searchParams.keys()].length ||
|
||||
currentSearchParameters.some(([key, value]) => newLocation.searchParams.get(key) !== value)
|
||||
|
||||
if (needsUpdate) {
|
||||
const entry = {
|
||||
...location,
|
||||
search: formatSearchParameters(newSearchParameters),
|
||||
}
|
||||
|
||||
navigate(entry, { replace })
|
||||
navigate(newLocation.toString(), { replace })
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,14 +5,7 @@ import type { Location, NavigateFunction, To } from 'react-router-dom'
|
||||
import { fromEvent, Subject, Subscription } from 'rxjs'
|
||||
import { filter } from 'rxjs/operators'
|
||||
|
||||
import {
|
||||
addLineRangeQueryParameter,
|
||||
formatSearchParameters,
|
||||
lprToRange,
|
||||
toPositionOrRangeQueryParameter,
|
||||
toViewStateHash,
|
||||
} from '@sourcegraph/common'
|
||||
import { parseQueryAndHash } from '@sourcegraph/shared/src/util/url'
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
import { Icon, Tooltip } from '@sourcegraph/wildcard'
|
||||
|
||||
import { eventLogger } from '../../../tracking/eventLogger'
|
||||
@ -40,7 +33,7 @@ export class ToggleHistoryPanel extends React.PureComponent<
|
||||
* Reports the current visibility (derived from the location).
|
||||
*/
|
||||
public static isVisible(location: Location): boolean {
|
||||
return parseQueryAndHash<BlobPanelTabID>(location.search, location.hash).viewState === 'history'
|
||||
return SourcegraphURL.from(location).viewState === 'history'
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,19 +41,10 @@ export class ToggleHistoryPanel extends React.PureComponent<
|
||||
* the given value.
|
||||
*/
|
||||
private static locationWithVisibility(location: Location, visible: boolean): To {
|
||||
const parsedQuery = parseQueryAndHash<BlobPanelTabID>(location.search, location.hash)
|
||||
if (visible) {
|
||||
parsedQuery.viewState = 'history' // defaults to last-viewed tab, or first tab
|
||||
} else {
|
||||
delete parsedQuery.viewState
|
||||
}
|
||||
const lineRangeQueryParameter = toPositionOrRangeQueryParameter({ range: lprToRange(parsedQuery) })
|
||||
|
||||
const url = SourcegraphURL.from(location).setViewState<BlobPanelTabID>(visible ? 'history' : undefined)
|
||||
return {
|
||||
search: formatSearchParameters(
|
||||
addLineRangeQueryParameter(new URLSearchParams(location.search), lineRangeQueryParameter)
|
||||
),
|
||||
hash: toViewStateHash(parsedQuery.viewState),
|
||||
search: url.search,
|
||||
hash: url.hash,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type * as H from 'history'
|
||||
|
||||
import { findLineKeyInSearchParameters } from '@sourcegraph/common'
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
import type { RenderMode } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
const URL_QUERY_PARAM = 'view'
|
||||
@ -21,19 +21,19 @@ export const getModeFromURL = (location: H.Location): RenderMode => {
|
||||
* Returns the URL that displays the blob using the specified mode.
|
||||
*/
|
||||
export const getURLForMode = (location: H.Location, mode: RenderMode): H.Location => {
|
||||
const searchParameters = new URLSearchParams(location.search)
|
||||
const url = SourcegraphURL.from(location)
|
||||
|
||||
if (mode === 'code') {
|
||||
searchParameters.set(URL_QUERY_PARAM, mode)
|
||||
url.setSearchParameter(URL_QUERY_PARAM, mode)
|
||||
} else {
|
||||
// We remove any existing line ranges as they are not supported in rendered mode.
|
||||
const existingLineRangeKey = findLineKeyInSearchParameters(searchParameters)
|
||||
if (existingLineRangeKey) {
|
||||
searchParameters.delete(existingLineRangeKey)
|
||||
}
|
||||
|
||||
searchParameters.delete(URL_QUERY_PARAM)
|
||||
url.setLineRange(null).deleteSearchParameter(URL_QUERY_PARAM)
|
||||
}
|
||||
|
||||
return { ...location, search: searchParameters.toString() }
|
||||
return {
|
||||
...location,
|
||||
pathname: url.pathname,
|
||||
search: url.search,
|
||||
hash: url.hash,
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { map } from 'rxjs/operators'
|
||||
|
||||
import { memoizeObservable } from '@sourcegraph/common'
|
||||
import { dataOrThrowErrors, gql } from '@sourcegraph/http-client'
|
||||
import { makeRepoURI } from '@sourcegraph/shared/src/util/url'
|
||||
import { makeRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import { requestGraphQL } from '../../backend/graphql'
|
||||
import {
|
||||
@ -37,7 +37,7 @@ const applyDefaultValuesToFetchBlobOptions = ({
|
||||
function fetchBlobCacheKey(options: FetchBlobOptions): string {
|
||||
const { disableTimeout, format, scipSnapshot, visibleIndexID } = applyDefaultValuesToFetchBlobOptions(options)
|
||||
|
||||
return `${makeRepoURI(
|
||||
return `${makeRepoGitURI(
|
||||
options
|
||||
)}?disableTimeout=${disableTimeout}&=${format}&snap=${scipSnapshot}&visible=${visibleIndexID}`
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
hasFindImplementationsSupport,
|
||||
} from '@sourcegraph/shared/src/codeintel/api'
|
||||
import type { Occurrence } from '@sourcegraph/shared/src/codeintel/scip'
|
||||
import { parseRepoURI, toURIWithPath } from '@sourcegraph/shared/src/util/url'
|
||||
import { makeRepoGitURI, parseRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
import type { UIRangeSpec } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import type { WebHoverOverlayProps } from '../../../../components/WebHoverOverlay'
|
||||
@ -117,7 +117,7 @@ export class CodeIntelAPIAdapter {
|
||||
private hasCodeIntelligenceSupport: boolean
|
||||
|
||||
constructor(private config: CodeIntelAPIConfig) {
|
||||
this.documentURI = toURIWithPath({
|
||||
this.documentURI = makeRepoGitURI({
|
||||
repoName: config.documentInfo.repoName,
|
||||
filePath: config.documentInfo.filePath,
|
||||
commitID: config.documentInfo.commitID,
|
||||
@ -173,7 +173,7 @@ export class CodeIntelAPIAdapter {
|
||||
)
|
||||
.then(locations => {
|
||||
for (const location of locations) {
|
||||
const { repoName, filePath, revision } = parseRepoURI(location.uri)
|
||||
const { repoName, filePath, revision } = parseRepoGitURI(location.uri)
|
||||
if (
|
||||
filePath &&
|
||||
location.range &&
|
||||
@ -197,7 +197,7 @@ export class CodeIntelAPIAdapter {
|
||||
}
|
||||
if (locations.length === 1) {
|
||||
const location = locations[0]
|
||||
const { repoName, filePath, revision } = parseRepoURI(location.uri)
|
||||
const { repoName, filePath, revision } = parseRepoGitURI(location.uri)
|
||||
if (!(filePath && location.range)) {
|
||||
return { type: 'none', occurrence }
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { map } from 'rxjs/operators'
|
||||
|
||||
import { memoizeObservable } from '@sourcegraph/common'
|
||||
import { dataOrThrowErrors, gql } from '@sourcegraph/http-client'
|
||||
import { makeRepoURI, type UIRange } from '@sourcegraph/shared/src/util/url'
|
||||
import { makeRepoGitURI, type UIRange } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import { requestGraphQL } from '../../backend/graphql'
|
||||
import type { DefinitionFields } from '../../graphql-operations'
|
||||
@ -149,6 +149,6 @@ export const fetchDefinitionsFromRanges = memoizeObservable(
|
||||
},
|
||||
options => {
|
||||
const { repoName, revision, filePath, ranges } = options
|
||||
return `${makeRepoURI({ repoName, revision, filePath })}?${ranges.map(buildRangeKey).join(',')}`
|
||||
return `${makeRepoGitURI({ repoName, revision, filePath })}?${ranges.map(buildRangeKey).join(',')}`
|
||||
}
|
||||
)
|
||||
|
||||
@ -5,14 +5,14 @@ import { type Observable, Subscription } from 'rxjs'
|
||||
|
||||
import { type Panel, useBuiltinTabbedPanelViews } from '@sourcegraph/branded/src/components/panel/TabbedPanelContent'
|
||||
import { PanelContent } from '@sourcegraph/branded/src/components/panel/views/PanelContent'
|
||||
import { isDefined, isErrorLike } from '@sourcegraph/common'
|
||||
import { SourcegraphURL, isDefined, isErrorLike } from '@sourcegraph/common'
|
||||
import type { FetchFileParameters } from '@sourcegraph/shared/src/backend/file'
|
||||
import type { Scalars } from '@sourcegraph/shared/src/graphql-operations'
|
||||
import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
import type { Settings, SettingsCascadeOrError, SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
|
||||
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
|
||||
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { type AbsoluteRepoFile, type ModeSpec, parseQueryAndHash } from '@sourcegraph/shared/src/util/url'
|
||||
import { type AbsoluteRepoFile, type ModeSpec } from '@sourcegraph/shared/src/util/url'
|
||||
import { Text } from '@sourcegraph/wildcard'
|
||||
|
||||
import type { CodeIntelligenceProps } from '../../../codeintel'
|
||||
@ -64,11 +64,9 @@ function useBlobPanelViews({
|
||||
const location = useLocation()
|
||||
|
||||
const position = useMemo(() => {
|
||||
const parsedHash = parseQueryAndHash(location.search, location.hash)
|
||||
return parsedHash.line !== undefined
|
||||
? { line: parsedHash.line, character: parsedHash.character || 0 }
|
||||
: undefined
|
||||
}, [location.hash, location.search])
|
||||
const lineRange = SourcegraphURL.from(location).lineRange
|
||||
return lineRange.line !== undefined ? { line: lineRange.line, character: lineRange.character || 0 } : undefined
|
||||
}, [location])
|
||||
|
||||
const [enableOwnershipPanels] = useFeatureFlag('enable-ownership-panels', true)
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { mdiChevronDown, mdiChevronUp, mdiArrowRight } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { smartSearchIconSvgPath, SyntaxHighlightedSearchQuery } from '@sourcegraph/branded'
|
||||
import { pluralize, formatSearchParameters } from '@sourcegraph/common'
|
||||
import { pluralize, SourcegraphURL } from '@sourcegraph/common'
|
||||
import type {
|
||||
AggregateStreamingSearchResults,
|
||||
AlertKind,
|
||||
@ -131,10 +131,12 @@ export const SmartSearch: React.FunctionComponent<React.PropsWithChildren<SmartS
|
||||
{alert?.proposedQueries?.map(entry => (
|
||||
<li key={entry.query} className={styles.listItem}>
|
||||
<Link
|
||||
to={createLinkUrl({
|
||||
pathname: '/search',
|
||||
search: formatSearchParameters(new URLSearchParams({ q: entry.query })),
|
||||
})}
|
||||
to={createLinkUrl(
|
||||
SourcegraphURL.from({
|
||||
pathname: '/search',
|
||||
search: new URLSearchParams({ q: entry.query }),
|
||||
})
|
||||
)}
|
||||
className={styles.link}
|
||||
>
|
||||
<Text className="mb-0">
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { type LineOrPositionOrRange, lprToRange, toPositionHashComponent } from '@sourcegraph/common'
|
||||
import { SourcegraphURL } from '@sourcegraph/common'
|
||||
import type { Position, Range } from '@sourcegraph/extension-api-types'
|
||||
import {
|
||||
encodeRepoRevision,
|
||||
type ParsedRepoRevision,
|
||||
type ParsedRepoURI,
|
||||
parseQueryAndHash,
|
||||
parseRepoRevision,
|
||||
type RepoFile,
|
||||
} from '@sourcegraph/shared/src/util/url'
|
||||
@ -13,31 +12,6 @@ export function toTreeURL(target: RepoFile): string {
|
||||
return `/${encodeRepoRevision(target)}/-/tree/${target.filePath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given URLSearchParams as a string.
|
||||
*/
|
||||
export function formatHash(searchParameters: URLSearchParams): string {
|
||||
const anyParameters = [...searchParameters].length > 0
|
||||
return `${anyParameters ? '#' + searchParameters.toString() : ''}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the textual form of the LineOrPositionOrRange suitable for encoding
|
||||
* in a URL fragment' query parameter.
|
||||
*
|
||||
* @param lpr The `LineOrPositionOrRange`
|
||||
*/
|
||||
export function formatLineOrPositionOrRange(lpr: LineOrPositionOrRange): string | undefined {
|
||||
const range = lprToRange(lpr)
|
||||
if (!range) {
|
||||
return undefined
|
||||
}
|
||||
const emptyRange = range.start.line === range.end.line && range.start.character === range.end.character
|
||||
return emptyRange
|
||||
? toPositionHashComponent(range.start)
|
||||
: `${toPositionHashComponent(range.start)}-${toPositionHashComponent(range.end)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the revision in the given URL, or adds one if there is not already
|
||||
* one.
|
||||
@ -59,7 +33,7 @@ export function replaceRevisionInURL(href: string, newRevision: string): string
|
||||
* Parses the properties of a blob URL.
|
||||
*/
|
||||
export function parseBrowserRepoURL(href: string): ParsedRepoURI & Pick<ParsedRepoRevision, 'rawRevision'> {
|
||||
const url = new URL(href, window.location.href)
|
||||
const url = SourcegraphURL.from(href)
|
||||
let pathname = url.pathname.slice(1) // trim leading '/'
|
||||
if (pathname.endsWith('/')) {
|
||||
pathname = pathname.slice(0, -1) // trim trailing '/'
|
||||
@ -111,18 +85,18 @@ export function parseBrowserRepoURL(href: string): ParsedRepoURI & Pick<ParsedRe
|
||||
let position: Position | undefined
|
||||
let range: Range | undefined
|
||||
|
||||
const parsedHash = parseQueryAndHash(url.search, url.hash)
|
||||
if (parsedHash.line) {
|
||||
const lineRange = url.lineRange
|
||||
if (lineRange.line) {
|
||||
position = {
|
||||
line: parsedHash.line,
|
||||
character: parsedHash.character || 0,
|
||||
line: lineRange.line,
|
||||
character: lineRange.character || 0,
|
||||
}
|
||||
if (parsedHash.endLine) {
|
||||
if (lineRange.endLine) {
|
||||
range = {
|
||||
start: position,
|
||||
end: {
|
||||
line: parsedHash.endLine,
|
||||
character: parsedHash.endCharacter || 0,
|
||||
line: lineRange.endLine,
|
||||
character: lineRange.endCharacter || 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,7 +214,7 @@
|
||||
"happy-dom": "^12.10.1",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"json-schema-ref-parser": "^9.0.6",
|
||||
"json-schema-to-typescript": "^10.1.3",
|
||||
"latest-version": "^5.1.0",
|
||||
|
||||
148
pnpm-lock.yaml
148
pnpm-lock.yaml
@ -927,8 +927,8 @@ importers:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
jsdom:
|
||||
specifier: ^22.1.0
|
||||
version: 22.1.0
|
||||
specifier: ^24.0.0
|
||||
version: 24.0.0
|
||||
json-schema-ref-parser:
|
||||
specifier: ^9.0.6
|
||||
version: 9.0.6
|
||||
@ -1069,7 +1069,7 @@ importers:
|
||||
version: 1.0.3
|
||||
vitest:
|
||||
specifier: 1.0.0-beta.4
|
||||
version: 1.0.0-beta.4(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@22.1.0)(sass@1.32.4)
|
||||
version: 1.0.0-beta.4(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@24.0.0)(sass@1.32.4)
|
||||
vitest-fetch-mock:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2(vitest@1.0.0-beta.4)
|
||||
@ -1616,7 +1616,7 @@ importers:
|
||||
version: 0.7.35(vite@5.1.5)
|
||||
vitest:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@22.1.0)(sass@1.32.4)
|
||||
version: 1.3.1(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@24.0.0)(sass@1.32.4)
|
||||
|
||||
client/wildcard:
|
||||
dependencies:
|
||||
@ -10733,7 +10733,7 @@ packages:
|
||||
dom-accessibility-api: 0.5.16
|
||||
lodash: 4.17.21
|
||||
redent: 3.0.0
|
||||
vitest: 1.0.0-beta.4(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@22.1.0)(sass@1.32.4)
|
||||
vitest: 1.0.0-beta.4(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@24.0.0)(sass@1.32.4)
|
||||
dev: true
|
||||
|
||||
/@testing-library/react-hooks@8.0.0(@types/react@18.0.8)(react-dom@18.1.0)(react@18.1.0):
|
||||
@ -12525,11 +12525,6 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/abab@2.0.6:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
deprecated: Use your platform's native atob() and btoa() methods instead
|
||||
dev: true
|
||||
|
||||
/abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
dev: true
|
||||
@ -14459,9 +14454,9 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
/cssstyle@3.0.0:
|
||||
resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==}
|
||||
engines: {node: '>=14'}
|
||||
/cssstyle@4.0.1:
|
||||
resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
rrweb-cssom: 0.6.0
|
||||
dev: true
|
||||
@ -14581,13 +14576,12 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/data-urls@4.0.0:
|
||||
resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==}
|
||||
engines: {node: '>=14'}
|
||||
/data-urls@5.0.0:
|
||||
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
abab: 2.0.6
|
||||
whatwg-mimetype: 3.0.0
|
||||
whatwg-url: 12.0.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.0.0
|
||||
dev: true
|
||||
|
||||
/dataloader@2.1.0:
|
||||
@ -15026,14 +15020,6 @@ packages:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
dev: true
|
||||
|
||||
/domexception@4.0.0:
|
||||
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
dependencies:
|
||||
webidl-conversions: 7.0.0
|
||||
dev: true
|
||||
|
||||
/domhandler@3.3.0:
|
||||
resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==}
|
||||
engines: {node: '>= 4'}
|
||||
@ -17645,11 +17631,11 @@ packages:
|
||||
resolution: {integrity: sha512-ygFIdTqqwG4fFP7kkiYlvayZppeIQX2aPpirsngkv1xM1lP0piDY5QEh68nQnIKvz64hfocxhBaD/uK3sSK1yQ==}
|
||||
dev: false
|
||||
|
||||
/html-encoding-sniffer@3.0.0:
|
||||
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
|
||||
engines: {node: '>=12'}
|
||||
/html-encoding-sniffer@4.0.0:
|
||||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
whatwg-encoding: 2.0.0
|
||||
whatwg-encoding: 3.1.1
|
||||
dev: true
|
||||
|
||||
/html-escaper@2.0.0:
|
||||
@ -18793,38 +18779,36 @@ packages:
|
||||
engines: {node: '>=12.0.0'}
|
||||
dev: true
|
||||
|
||||
/jsdom@22.1.0:
|
||||
resolution: {integrity: sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==}
|
||||
engines: {node: '>=16'}
|
||||
/jsdom@24.0.0:
|
||||
resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
canvas: ^2.5.0
|
||||
canvas: ^2.11.2
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
dependencies:
|
||||
abab: 2.0.6
|
||||
cssstyle: 3.0.0
|
||||
data-urls: 4.0.0
|
||||
cssstyle: 4.0.1
|
||||
data-urls: 5.0.0
|
||||
decimal.js: 10.4.3
|
||||
domexception: 4.0.0
|
||||
form-data: 4.0.0
|
||||
html-encoding-sniffer: 3.0.0
|
||||
http-proxy-agent: 5.0.0
|
||||
https-proxy-agent: 5.0.1
|
||||
html-encoding-sniffer: 4.0.0
|
||||
http-proxy-agent: 7.0.0
|
||||
https-proxy-agent: 7.0.2
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.7
|
||||
parse5: 7.1.2
|
||||
rrweb-cssom: 0.6.0
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 4.1.2
|
||||
w3c-xmlserializer: 4.0.0
|
||||
tough-cookie: 4.1.3
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 2.0.0
|
||||
whatwg-mimetype: 3.0.0
|
||||
whatwg-url: 12.0.1
|
||||
ws: 8.14.2
|
||||
xml-name-validator: 4.0.0
|
||||
whatwg-encoding: 3.1.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.0.0
|
||||
ws: 8.16.0
|
||||
xml-name-validator: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
@ -24620,8 +24604,8 @@ packages:
|
||||
nopt: 1.0.10
|
||||
dev: true
|
||||
|
||||
/tough-cookie@4.1.2:
|
||||
resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==}
|
||||
/tough-cookie@4.1.3:
|
||||
resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
psl: 1.8.0
|
||||
@ -24633,19 +24617,11 @@ packages:
|
||||
/tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
/tr46@4.1.1:
|
||||
resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
dev: true
|
||||
|
||||
/tr46@5.0.0:
|
||||
resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
dev: false
|
||||
|
||||
/tree-kill@1.2.2:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
@ -25531,12 +25507,12 @@ packages:
|
||||
vitest: '>=0.16.0'
|
||||
dependencies:
|
||||
cross-fetch: 3.1.6
|
||||
vitest: 1.0.0-beta.4(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@22.1.0)(sass@1.32.4)
|
||||
vitest: 1.0.0-beta.4(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@24.0.0)(sass@1.32.4)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/vitest@1.0.0-beta.4(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@22.1.0)(sass@1.32.4):
|
||||
/vitest@1.0.0-beta.4(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@24.0.0)(sass@1.32.4):
|
||||
resolution: {integrity: sha512-WOJTqxY3hWqn4yy26SK+cx+BlPBeK/KtY9ALWkD6FLWLhSGY0QFEmarc8sdb/UGZQ8xs5pOvcQQS9JJSV8HH8g==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
@ -25573,7 +25549,7 @@ packages:
|
||||
chai: 4.3.10
|
||||
debug: 4.3.4
|
||||
happy-dom: 12.10.1
|
||||
jsdom: 22.1.0
|
||||
jsdom: 24.0.0
|
||||
local-pkg: 0.4.3
|
||||
magic-string: 0.30.5
|
||||
pathe: 1.1.2
|
||||
@ -25595,7 +25571,7 @@ packages:
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vitest@1.3.1(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@22.1.0)(sass@1.32.4):
|
||||
/vitest@1.3.1(@types/node@20.8.0)(happy-dom@12.10.1)(jsdom@24.0.0)(sass@1.32.4):
|
||||
resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
@ -25631,7 +25607,7 @@ packages:
|
||||
debug: 4.3.4
|
||||
execa: 8.0.1
|
||||
happy-dom: 12.10.1
|
||||
jsdom: 22.1.0
|
||||
jsdom: 24.0.0
|
||||
local-pkg: 0.5.0
|
||||
magic-string: 0.30.5
|
||||
pathe: 1.1.2
|
||||
@ -25711,11 +25687,11 @@ packages:
|
||||
resolution: {integrity: sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==}
|
||||
dev: false
|
||||
|
||||
/w3c-xmlserializer@4.0.0:
|
||||
resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}
|
||||
engines: {node: '>=14'}
|
||||
/w3c-xmlserializer@5.0.0:
|
||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
xml-name-validator: 4.0.0
|
||||
xml-name-validator: 5.0.0
|
||||
dev: true
|
||||
|
||||
/walker@1.0.8:
|
||||
@ -25855,6 +25831,13 @@ packages:
|
||||
iconv-lite: 0.6.3
|
||||
dev: true
|
||||
|
||||
/whatwg-encoding@3.1.1:
|
||||
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
dev: true
|
||||
|
||||
/whatwg-fetch@3.6.20:
|
||||
resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
|
||||
|
||||
@ -25863,12 +25846,9 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/whatwg-url@12.0.1:
|
||||
resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
tr46: 4.1.1
|
||||
webidl-conversions: 7.0.0
|
||||
/whatwg-mimetype@4.0.0:
|
||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||
engines: {node: '>=18'}
|
||||
dev: true
|
||||
|
||||
/whatwg-url@14.0.0:
|
||||
@ -25877,7 +25857,6 @@ packages:
|
||||
dependencies:
|
||||
tr46: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
dev: false
|
||||
|
||||
/whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
@ -26126,6 +26105,19 @@ packages:
|
||||
optional: true
|
||||
dev: true
|
||||
|
||||
/ws@8.16.0:
|
||||
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
dev: true
|
||||
|
||||
/ws@8.5.0:
|
||||
resolution: {integrity: sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@ -26145,9 +26137,9 @@ packages:
|
||||
'@xmldom/xmldom': 0.8.10
|
||||
dev: false
|
||||
|
||||
/xml-name-validator@4.0.0:
|
||||
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
||||
engines: {node: '>=12'}
|
||||
/xml-name-validator@5.0.0:
|
||||
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
|
||||
engines: {node: '>=18'}
|
||||
dev: true
|
||||
|
||||
/xml2js@0.4.23:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user