Skip to content

Commit 5123fe8

Browse files
authored
add noslash range and object marker parsing (#1175)
1 parent 8e7cc78 commit 5123fe8

File tree

5 files changed

+228
-112
lines changed

5 files changed

+228
-112
lines changed

internal/fourslash/fourslash.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, conten
129129
testfs := make(map[string]string)
130130
testData := ParseTestData(t, content, fileName)
131131
for _, file := range testData.Files {
132-
filePath := tspath.GetNormalizedAbsolutePath(file.Filename, rootDir)
132+
filePath := tspath.GetNormalizedAbsolutePath(file.fileName, rootDir)
133133
testfs[filePath] = file.Content
134134
}
135135

@@ -276,9 +276,9 @@ func (f *FourslashTest) GoToMarker(t *testing.T, markerName string) {
276276
if !ok {
277277
t.Fatalf("Marker %s not found", markerName)
278278
}
279-
f.ensureActiveFile(t, marker.Filename)
279+
f.ensureActiveFile(t, marker.FileName)
280280
f.currentCaretPosition = marker.LSPosition
281-
f.currentFilename = marker.Filename
281+
f.currentFilename = marker.FileName
282282
f.lastKnownMarkerName = marker.Name
283283
}
284284

@@ -289,7 +289,7 @@ func (f *FourslashTest) Markers() []*Marker {
289289
func (f *FourslashTest) ensureActiveFile(t *testing.T, filename string) {
290290
if f.activeFilename != filename {
291291
file := core.Find(f.testData.Files, func(f *TestFileInfo) bool {
292-
return f.Filename == filename
292+
return f.fileName == filename
293293
})
294294
if file == nil {
295295
t.Fatalf("File %s not found in test data", filename)
@@ -299,11 +299,11 @@ func (f *FourslashTest) ensureActiveFile(t *testing.T, filename string) {
299299
}
300300

301301
func (f *FourslashTest) openFile(t *testing.T, file *TestFileInfo) {
302-
f.activeFilename = file.Filename
302+
f.activeFilename = file.fileName
303303
f.sendNotification(t, lsproto.MethodTextDocumentDidOpen, &lsproto.DidOpenTextDocumentParams{
304304
TextDocument: &lsproto.TextDocumentItem{
305-
Uri: ls.FileNameToDocumentURI(file.Filename),
306-
LanguageId: getLanguageKind(file.Filename),
305+
Uri: ls.FileNameToDocumentURI(file.fileName),
306+
LanguageId: getLanguageKind(file.fileName),
307307
Text: file.Content,
308308
},
309309
})

internal/fourslash/test_parser.go

Lines changed: 145 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fourslash
22

33
import (
4+
"encoding/json"
45
"strings"
56
"testing"
67
"unicode/utf8"
@@ -19,18 +20,18 @@ import (
1920
// [|text in range|]
2021
//
2122
// is a range with `text in range` "selected".
22-
type markerRange struct {
23-
core.TextRange
24-
filename string
25-
position int
26-
data string
23+
type RangeMarker struct {
24+
*Marker
25+
Range core.TextRange
26+
LSRange lsproto.Range
2727
}
2828

2929
type Marker struct {
30-
Filename string
30+
FileName string
3131
Position int
3232
LSPosition lsproto.Position
3333
Name string
34+
Data map[string]interface{}
3435
}
3536

3637
type TestData struct {
@@ -39,12 +40,13 @@ type TestData struct {
3940
Markers []*Marker
4041
Symlinks map[string]string
4142
GlobalOptions map[string]string
42-
Ranges []*markerRange
43+
Ranges []*RangeMarker
4344
}
4445

4546
type testFileWithMarkers struct {
4647
file *TestFileInfo
4748
markers []*Marker
49+
ranges []*RangeMarker
4850
}
4951

5052
func ParseTestData(t *testing.T, contents string, fileName string) TestData {
@@ -53,6 +55,7 @@ func ParseTestData(t *testing.T, contents string, fileName string) TestData {
5355

5456
markerPositions := make(map[string]*Marker)
5557
var markers []*Marker
58+
var ranges []*RangeMarker
5659
filesWithMarker, symlinks, _, globalOptions := testrunner.ParseTestFilesAndSymlinks(
5760
contents,
5861
fileName,
@@ -62,14 +65,17 @@ func ParseTestData(t *testing.T, contents string, fileName string) TestData {
6265
hasTSConfig := false
6366
for _, file := range filesWithMarker {
6467
files = append(files, file.file)
65-
hasTSConfig = hasTSConfig || isConfigFile(file.file.Filename)
68+
hasTSConfig = hasTSConfig || isConfigFile(file.file.fileName)
69+
6670
markers = append(markers, file.markers...)
71+
ranges = append(ranges, file.ranges...)
6772
for _, marker := range file.markers {
6873
if _, ok := markerPositions[marker.Name]; ok {
6974
t.Fatalf("Duplicate marker name: %s", marker.Name)
7075
}
7176
markerPositions[marker.Name] = marker
7277
}
78+
7379
}
7480

7581
if hasTSConfig && len(globalOptions) > 0 {
@@ -82,13 +88,13 @@ func ParseTestData(t *testing.T, contents string, fileName string) TestData {
8288
Markers: markers,
8389
Symlinks: symlinks,
8490
GlobalOptions: globalOptions,
85-
Ranges: nil,
91+
Ranges: ranges,
8692
}
8793
}
8894

89-
func isConfigFile(filename string) bool {
90-
filename = strings.ToLower(filename)
91-
return strings.HasSuffix(filename, "tsconfig.json") || strings.HasSuffix(filename, "jsconfig.json")
95+
func isConfigFile(fileName string) bool {
96+
fileName = strings.ToLower(fileName)
97+
return strings.HasSuffix(fileName, "tsconfig.json") || strings.HasSuffix(fileName, "jsconfig.json")
9298
}
9399

94100
type locationInformation struct {
@@ -98,16 +104,21 @@ type locationInformation struct {
98104
sourceColumn int
99105
}
100106

107+
type rangeLocationInformation struct {
108+
locationInformation
109+
marker *Marker
110+
}
111+
101112
type TestFileInfo struct {
102-
Filename string
113+
fileName string
103114
// The contents of the file (with markers, etc stripped out)
104115
Content string
105116
emit bool
106117
}
107118

108119
// FileName implements ls.Script.
109120
func (t *TestFileInfo) FileName() string {
110-
return t.Filename
121+
return t.fileName
111122
}
112123

113124
// Text implements ls.Script.
@@ -127,14 +138,19 @@ const (
127138
stateInObjectMarker
128139
)
129140

130-
func parseFileContent(filename string, content string, fileOptions map[string]string) *testFileWithMarkers {
131-
filename = tspath.GetNormalizedAbsolutePath(filename, "/")
141+
func parseFileContent(fileName string, content string, fileOptions map[string]string) *testFileWithMarkers {
142+
fileName = tspath.GetNormalizedAbsolutePath(fileName, "/")
132143

133144
// The file content (minus metacharacters) so far
134145
var output strings.Builder
135146

136147
var markers []*Marker
137148

149+
/// A stack of the open range markers that are still unclosed
150+
openRanges := []rangeLocationInformation{}
151+
/// A list of closed ranges we've collected so far
152+
rangeMarkers := []*RangeMarker{}
153+
138154
// The total number of metacharacters removed from the file (so far)
139155
difference := 0
140156

@@ -164,9 +180,43 @@ func parseFileContent(filename string, content string, fileOptions map[string]st
164180
currentCharacter, size = utf8.DecodeRuneInString(content[i:])
165181
switch state {
166182
case stateNone:
167-
// !!! case '[', '|'
168-
// !!! case '|', ']'
169-
if previousCharacter == '/' && currentCharacter == '*' {
183+
if previousCharacter == '[' && currentCharacter == '|' {
184+
// found a range start
185+
openRanges = append(openRanges, rangeLocationInformation{
186+
locationInformation: locationInformation{
187+
position: (i - 1) - difference,
188+
sourcePosition: i - 1,
189+
sourceLine: line,
190+
sourceColumn: column,
191+
},
192+
})
193+
// copy all text up to marker position
194+
flush(i - 1)
195+
lastNormalCharPosition = i + 1
196+
difference += 2
197+
} else if previousCharacter == '|' && currentCharacter == ']' {
198+
// found a range end
199+
if len(openRanges) == 0 {
200+
reportError(fileName, line, column, "Found range end with no matching start.")
201+
}
202+
rangeStart := openRanges[len(openRanges)-1]
203+
openRanges = openRanges[:len(openRanges)-1]
204+
205+
closedRange := &RangeMarker{Range: core.NewTextRange(rangeStart.position, (i-1)-difference)}
206+
if rangeStart.marker != nil {
207+
closedRange.Marker = rangeStart.marker
208+
} else {
209+
// RangeMarker is not added to list of markers
210+
closedRange.Marker = &Marker{FileName: fileName}
211+
}
212+
213+
rangeMarkers = append(rangeMarkers, closedRange)
214+
215+
// copy all text up to range marker position
216+
flush(i - 1)
217+
lastNormalCharPosition = i + 1
218+
difference += 2
219+
} else if previousCharacter == '/' && currentCharacter == '*' {
170220
// found a possible marker start
171221
state = stateInSlashStarMarker
172222
openMarker = locationInformation{
@@ -175,20 +225,49 @@ func parseFileContent(filename string, content string, fileOptions map[string]st
175225
sourceLine: line,
176226
sourceColumn: column - 1,
177227
}
228+
} else if previousCharacter == '{' && currentCharacter == '|' {
229+
// found an object marker start
230+
state = stateInObjectMarker
231+
openMarker = locationInformation{
232+
position: (i - 1) - difference,
233+
sourcePosition: i - 1,
234+
sourceLine: line,
235+
sourceColumn: column,
236+
}
237+
flush(i - 1)
178238
}
179-
// !!! case '{', '|'
180239
case stateInObjectMarker:
181-
// !!! object marker
240+
// Object markers are only ever terminated by |} and have no content restrictions
241+
if previousCharacter == '|' && currentCharacter == '}' {
242+
objectMarkerData := strings.TrimSpace(content[openMarker.sourcePosition+2 : i-1])
243+
marker := getObjectMarker(fileName, openMarker, objectMarkerData)
244+
245+
if len(openRanges) > 0 {
246+
openRanges[len(openRanges)-1].marker = marker
247+
}
248+
markers = append(markers, marker)
249+
250+
// Set the current start to point to the end of the current marker to ignore its text
251+
lastNormalCharPosition = i + 1
252+
difference += i + 1 - openMarker.sourcePosition
253+
254+
// Reset the state
255+
openMarker = locationInformation{}
256+
state = stateNone
257+
}
182258
case stateInSlashStarMarker:
183259
if previousCharacter == '*' && currentCharacter == '/' {
184260
// Record the marker
185261
// start + 2 to ignore the */, -1 on the end to ignore the * (/ is next)
186262
markerNameText := strings.TrimSpace(content[openMarker.sourcePosition+2 : i-1])
187263
marker := &Marker{
188-
Filename: filename,
264+
FileName: fileName,
189265
Position: openMarker.position,
190266
Name: markerNameText,
191267
}
268+
if len(openRanges) > 0 {
269+
openRanges[len(openRanges)-1].marker = marker
270+
}
192271
markers = append(markers, marker)
193272

194273
// Set the current start to point to the end of the current marker to ignore its text
@@ -240,17 +319,60 @@ func parseFileContent(filename string, content string, fileOptions map[string]st
240319
emit := fileOptions[emitThisFileOption] == "true"
241320

242321
testFileInfo := &TestFileInfo{
243-
Filename: filename,
322+
fileName: fileName,
244323
Content: outputString,
245324
emit: emit,
246325
}
247326

248327
for _, marker := range markers {
249328
marker.LSPosition = converters.PositionToLineAndCharacter(testFileInfo, core.TextPos(marker.Position))
250329
}
330+
for _, rangeMarker := range rangeMarkers {
331+
rangeMarker.LSRange = lsproto.Range{
332+
Start: converters.PositionToLineAndCharacter(testFileInfo, core.TextPos(rangeMarker.Range.Pos())),
333+
End: converters.PositionToLineAndCharacter(testFileInfo, core.TextPos(rangeMarker.Range.End())),
334+
}
335+
}
251336

252337
return &testFileWithMarkers{
253338
file: testFileInfo,
254339
markers: markers,
340+
ranges: rangeMarkers,
255341
}
256342
}
343+
344+
func getObjectMarker(fileName string, location locationInformation, text string) *Marker {
345+
// Attempt to parse the marker value as JSON
346+
var v interface{}
347+
e := json.Unmarshal([]byte("{ "+text+" }"), &v)
348+
349+
if e != nil {
350+
reportError(fileName, location.sourceLine, location.sourceColumn, "Unable to parse marker text "+text)
351+
return nil
352+
}
353+
markerValue, ok := v.(map[string]interface{})
354+
if !ok || len(markerValue) == 0 {
355+
reportError(fileName, location.sourceLine, location.sourceColumn, "Object markers can not be empty")
356+
return nil
357+
}
358+
359+
marker := &Marker{
360+
FileName: fileName,
361+
Position: location.position,
362+
Data: markerValue,
363+
}
364+
365+
// Object markers can be anonymous
366+
if markerValue["name"] != nil {
367+
if name, ok := markerValue["name"].(string); ok && name != "" {
368+
marker.Name = name
369+
}
370+
}
371+
372+
return marker
373+
}
374+
375+
func reportError(fileName string, line int, col int, message string) {
376+
// !!! not implemented
377+
// errorMessage := fileName + "(" + string(line) + "," + string(col) + "): " + message;
378+
}

internal/ls/completions.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,10 +314,10 @@ func (l *LanguageService) getCompletionsAtPosition(
314314
return stringCompletions
315315
}
316316

317-
if previousToken != nil && ast.IsBreakOrContinueStatement(previousToken.Parent) &&
318-
(previousToken.Kind == ast.KindBreakKeyword ||
319-
previousToken.Kind == ast.KindContinueKeyword ||
320-
previousToken.Kind == ast.KindIdentifier) {
317+
if previousToken != nil && (previousToken.Kind == ast.KindBreakKeyword ||
318+
previousToken.Kind == ast.KindContinueKeyword ||
319+
previousToken.Kind == ast.KindIdentifier) &&
320+
ast.IsBreakOrContinueStatement(previousToken.Parent) {
321321
return l.getLabelCompletionsAtPosition(previousToken.Parent, clientOptions, file, position)
322322
}
323323

0 commit comments

Comments
 (0)