Skip to content
This repository was archived by the owner on Nov 6, 2018. It is now read-only.

Commit ef2820b

Browse files
committed
feat: parse and expose URIs in extension API
The enhanced sourcegraph.URI class (and its implementation, which is shared by the client and extension) is a full URI parser. Previously it only wrapped a string.
1 parent 8d707f9 commit ef2820b

File tree

6 files changed

+274
-22
lines changed

6 files changed

+274
-22
lines changed

src/extension/extensionHost.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ function createExtensionHandle(initData: InitData, connection: Connection): type
156156
internal: {
157157
sync,
158158
updateContext: updates => context.updateContext(updates),
159-
sourcegraphURL: new URI(initData.sourcegraphURL),
159+
sourcegraphURL: URI.parse(initData.sourcegraphURL),
160160
clientApplication: initData.clientApplication,
161161
},
162162
}

src/extension/types/location.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { Range } from './range'
66

77
describe('Location', () => {
88
it('toJSON', () => {
9-
assertToJSON(new Location(URI.file('u.ts'), new Position(3, 4)), {
10-
uri: URI.parse('file://u.ts').toJSON(),
9+
assertToJSON(new Location(URI.parse('file:///u.ts'), new Position(3, 4)), {
10+
uri: URI.parse('file:///u.ts').toJSON(),
1111
range: { start: { line: 3, character: 4 }, end: { line: 3, character: 4 } },
1212
})
13-
assertToJSON(new Location(URI.file('u.ts'), new Range(1, 2, 3, 4)), {
14-
uri: URI.parse('file://u.ts').toJSON(),
13+
assertToJSON(new Location(URI.parse('file:///u.ts'), new Range(1, 2, 3, 4)), {
14+
uri: URI.parse('file:///u.ts').toJSON(),
1515
range: { start: { line: 1, character: 2 }, end: { line: 3, character: 4 } },
1616
})
1717
})

src/integration-test/languageFeatures.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('LanguageFeatures (integration)', () => {
3131
extensionHost => extensionHost.languages.registerDefinitionProvider,
3232
label =>
3333
({
34-
provideDefinition: (doc, pos) => [{ uri: new URI(`file:///${label}`) }],
34+
provideDefinition: (doc, pos) => [{ uri: URI.parse(`file:///${label}`) }],
3535
} as sourcegraph.DefinitionProvider),
3636
labeledDefinitionResults,
3737
run => ({ provideDefinition: run } as sourcegraph.DefinitionProvider),
@@ -46,7 +46,7 @@ describe('LanguageFeatures (integration)', () => {
4646
extensionHost => extensionHost.languages.registerTypeDefinitionProvider,
4747
label =>
4848
({
49-
provideTypeDefinition: (doc, pos) => [{ uri: new URI(`file:///${label}`) }],
49+
provideTypeDefinition: (doc, pos) => [{ uri: URI.parse(`file:///${label}`) }],
5050
} as sourcegraph.TypeDefinitionProvider),
5151
labeledDefinitionResults,
5252
run => ({ provideTypeDefinition: run } as sourcegraph.TypeDefinitionProvider),
@@ -61,7 +61,7 @@ describe('LanguageFeatures (integration)', () => {
6161
extensionHost => extensionHost.languages.registerImplementationProvider,
6262
label =>
6363
({
64-
provideImplementation: (doc, pos) => [{ uri: new URI(`file:///${label}`) }],
64+
provideImplementation: (doc, pos) => [{ uri: URI.parse(`file:///${label}`) }],
6565
} as sourcegraph.ImplementationProvider),
6666
labeledDefinitionResults,
6767
run => ({ provideImplementation: run } as sourcegraph.ImplementationProvider),
@@ -76,7 +76,7 @@ describe('LanguageFeatures (integration)', () => {
7676
extensionHost => extensionHost.languages.registerReferenceProvider,
7777
label =>
7878
({
79-
provideReferences: (doc, pos, context) => [{ uri: new URI(`file:///${label}`) }],
79+
provideReferences: (doc, pos, context) => [{ uri: URI.parse(`file:///${label}`) }],
8080
} as sourcegraph.ReferenceProvider),
8181
labels => labels.map(label => ({ uri: `file:///${label}`, range: undefined })),
8282
run => ({ provideReferences: run } as sourcegraph.ReferenceProvider),

src/shared/uri.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import assert from 'assert'
2+
import { URI, URIComponents } from './uri'
3+
4+
function assertParsedURI(uriStr: string, expected: Partial<URIComponents>, expectedStr = uriStr): void {
5+
expected.scheme = expected.scheme || ''
6+
expected.authority = expected.authority || ''
7+
expected.path = expected.path || ''
8+
expected.query = expected.query || ''
9+
expected.fragment = expected.fragment || ''
10+
11+
const uri = URI.parse(uriStr)
12+
assert.deepStrictEqual(uri.toJSON(), expected, `URI.parse(${JSON.stringify(uriStr)})`)
13+
14+
assert.strictEqual(uri.toString(), expectedStr, `URI#toString ${JSON.stringify(uri.toJSON())}`)
15+
}
16+
17+
describe('URI', () => {
18+
it('parses and produces string representation', () => {
19+
// A '/' is automatically added to the path for special schemes by Node's "url" package. This is
20+
// undesirable but harmless.
21+
assertParsedURI(
22+
'https://example.com',
23+
{
24+
scheme: 'https',
25+
authority: 'example.com',
26+
path: '/',
27+
},
28+
'https://example.com/'
29+
)
30+
assertParsedURI(
31+
'foo://example.com',
32+
{
33+
scheme: 'foo',
34+
authority: 'example.com',
35+
},
36+
'foo://example.com'
37+
)
38+
assertParsedURI('https://example.com/a', {
39+
scheme: 'https',
40+
authority: 'example.com',
41+
path: '/a',
42+
})
43+
assertParsedURI('foo://example.com/a', {
44+
scheme: 'foo',
45+
authority: 'example.com',
46+
path: '/a',
47+
})
48+
assertParsedURI('https://u:[email protected]:1234/a/b?c=d#e', {
49+
scheme: 'https',
50+
authority: 'u:[email protected]:1234',
51+
path: '/a/b',
52+
query: 'c=d',
53+
fragment: 'e',
54+
})
55+
assertParsedURI('file:///a', {
56+
scheme: 'file',
57+
path: '/a',
58+
})
59+
})
60+
61+
it('with', () =>
62+
assert.strictEqual(
63+
URI.parse('https://example.com/a')
64+
.with({ path: '/b', query: 'c' })
65+
.toString(),
66+
'https://example.com/b?c'
67+
))
68+
})

src/shared/uri.ts

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,141 @@
11
import * as sourcegraph from 'sourcegraph'
2+
import { URL } from 'url'
23

3-
export class URI implements sourcegraph.URI {
4+
/**
5+
* A serialized representation of a {@link URI} produced by {@link URI#toJSON} and that can be deserialized with
6+
* {@link URI.fromJSON}.
7+
*/
8+
export interface URIComponents {
9+
scheme: string
10+
authority: string
11+
path: string
12+
query: string
13+
fragment: string
14+
}
15+
16+
/**
17+
* A uniform resource identifier (URI), as defined in [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3).
18+
*
19+
* This URI class should be used instead of WHATWG URL or Node's "url" package because it parses all URI schemes in
20+
* the same way on all platforms. The WHATWG URL spec requires "non-special schemes" (anything other than http,
21+
* https, and a few other common schemes; see https://url.spec.whatwg.org/#special-scheme) to be parsed differently
22+
* in a way that is not desirable. For example:
23+
*
24+
* With WHATWG URL:
25+
*
26+
* new URL("https://foo/bar").pathname === "/bar"
27+
*
28+
* new URL("git://foo/bar").pathname === "//foo/bar" // UNDESIRABLE
29+
*
30+
* With Node's "url" package:
31+
*
32+
* new URL("https://foo/bar").pathname === "/bar"
33+
*
34+
* new URL("git://foo/bar").pathname === "/bar"
35+
*
36+
* Sourcegraph extensions generally intend to treat all URI schemes identically (as in the second example). This
37+
* class implements that behavior.
38+
*/
39+
export class URI implements sourcegraph.URI, URIComponents {
440
public static parse(uri: string): sourcegraph.URI {
5-
return new URI(uri)
41+
// Use Node's "url" package to parse the URI. When running in the browser, we assume the bundler (if any)
42+
// does the right thing and actually uses the Node "url" package instead of the browser WHATWG URL
43+
// implementation.
44+
const url = new URL(uri)
45+
return new URI({
46+
scheme: url.protocol.slice(0, -1), // omit trailing ':'
47+
authority: makeAuthority(url),
48+
path: url.pathname,
49+
query: url.search.slice(1), // omit leading '?'
50+
fragment: url.hash.slice(1), // omit leading '#'
51+
})
652
}
753

8-
public static file(path: string): sourcegraph.URI {
9-
return new URI(`file://${path}`)
54+
public static isURI(value: any): value is sourcegraph.URI {
55+
return (
56+
value instanceof URI ||
57+
(value &&
58+
(typeof value.scheme === 'string' &&
59+
typeof value.authority === 'string' &&
60+
typeof value.path === 'string' &&
61+
typeof value.query === 'string' &&
62+
typeof value.fragment === 'string'))
63+
)
1064
}
1165

12-
public static isURI(value: any): value is sourcegraph.URI {
13-
return value instanceof URI || typeof value === 'string'
66+
private constructor(components: URIComponents) {
67+
this.scheme = components.scheme
68+
this.authority = components.authority
69+
this.path = components.path
70+
this.query = components.query
71+
this.fragment = components.fragment
1472
}
1573

16-
constructor(private value: string) {}
74+
public readonly scheme: string
75+
76+
public readonly authority: string
77+
78+
public readonly path: string
79+
80+
public readonly query: string
81+
82+
public readonly fragment: string
83+
84+
public with(change: Partial<URIComponents>): URI {
85+
return new URI({ ...this.toJSON(), ...change })
86+
}
1787

1888
public toString(): string {
19-
return this.value
89+
let s = ''
90+
if (this.scheme) {
91+
s += this.scheme
92+
s += ':'
93+
}
94+
if (this.authority || this.scheme === 'file') {
95+
s += '//'
96+
}
97+
if (this.authority) {
98+
s += this.authority
99+
}
100+
if (this.path) {
101+
s += this.path
102+
}
103+
if (this.query) {
104+
s += '?'
105+
s += this.query
106+
}
107+
if (this.fragment) {
108+
s += '#'
109+
s += this.fragment
110+
}
111+
return s
112+
}
113+
114+
public toJSON(): URIComponents {
115+
return {
116+
scheme: this.scheme,
117+
authority: this.authority,
118+
path: this.path,
119+
query: this.query,
120+
fragment: this.fragment,
121+
}
122+
}
123+
124+
public static fromJSON(value: URIComponents): sourcegraph.URI {
125+
return new URI(value)
20126
}
127+
}
21128

22-
public toJSON(): any {
23-
return this.value
129+
function makeAuthority(url: URL): string {
130+
let s = ''
131+
if (url.username) {
132+
s += url.username
133+
}
134+
if (url.password) {
135+
s += `:${url.password}`
136+
}
137+
if (url.username || url.password) {
138+
s += '@'
24139
}
140+
return s + url.host
25141
}

src/sourcegraph.d.ts

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,88 @@ declare module 'sourcegraph' {
1010
unsubscribe(): void
1111
}
1212

13+
/**
14+
* A uniform resource identifier (URI), as defined in [RFC
15+
* 3986](https://tools.ietf.org/html/rfc3986#section-3).
16+
*
17+
* This URI implementation is preferred because the browser URL class implements the WHATWG URL spec, which
18+
* requires special treatment for certain URI schemes (such as http and https). That special treatment is
19+
* undesirable for Sourcegraph extensions, which need to treat URIs from any scheme in the same way.
20+
*/
1321
export class URI {
22+
/**
23+
* Parses a URI from its string representation.
24+
*/
1425
static parse(value: string): URI
15-
static file(path: string): URI
1626

17-
constructor(value: string)
27+
/**
28+
* Use {@link URI.parse} or {@link URI.fromJSON} to create {@link URI} instances.
29+
*/
30+
private constructor(args: never)
31+
32+
/**
33+
* The scheme component of the URI.
34+
*
35+
* @summary `https` in `https://example.com`
36+
* @see [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3)
37+
*/
38+
readonly scheme: string
39+
40+
/**
41+
* The authority component of the URI.
42+
*
43+
* @summary `example.com:1234` in `https://example.com:1234/a/b`
44+
* @see [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3)
45+
*/
46+
readonly authority: string
47+
48+
/**
49+
* The path component of the URI.
50+
*
51+
* @summary `/a/b` in `https://example.com/a/b`
52+
* @see [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3)
53+
*/
54+
readonly path: string
55+
56+
/**
57+
* The query component of the URI.
58+
*
59+
* @summary `b=c&d=e` in `https://example.com/a?b=c&d=e`
60+
* @see [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3)
61+
*/
62+
readonly query: string
63+
64+
/**
65+
* The fragment component of the URI.
66+
*
67+
* @summary `g` in `https://example.com/a#g`
68+
* @see [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3)
69+
*/
70+
readonly fragment: string
71+
72+
/**
73+
* Derives a new URI from this URI.
74+
*
75+
* @returns A copy of the URI with the changed components.
76+
*/
77+
with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): URI
1878

79+
/**
80+
* Returns the string representation of this URI.
81+
*/
1982
toString(): string
2083

2184
/**
22-
* Returns a JSON representation of this Uri.
85+
* Returns a JSON representation of this URI.
2386
*
2487
* @return An object.
2588
*/
2689
toJSON(): any
90+
91+
/**
92+
* Revives the URI from its JSON representation that was produced with {@link URI#toJSON}.
93+
*/
94+
static fromJSON(value: any): URI
2795
}
2896

2997
export class Position {

0 commit comments

Comments
 (0)