diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 53ee669474..ffe9baee60 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -2792,3 +2792,12 @@ func IsClassMemberModifier(token Kind) bool { func IsParameterPropertyModifier(kind Kind) bool { return ModifierToFlag(kind)&ModifierFlagsParameterPropertyModifier != 0 } + +func ForEachChildAndJSDoc(node *Node, sourceFile *SourceFile, v Visitor) bool { + if node.Flags&NodeFlagsHasJSDoc != 0 { + if visitNodes(v, node.JSDoc(sourceFile)) { + return true + } + } + return node.ForEachChild(v) +} diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 605ff714f7..b003ed90d3 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -121,7 +121,7 @@ const ( SortTextJavascriptIdentifiers sortText = "18" ) -func deprecateSortText(original sortText) sortText { +func DeprecateSortText(original sortText) sortText { return "z" + original } @@ -910,6 +910,7 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) useSemicolons := probablyUsesSemicolons(file) typeChecker := program.GetTypeChecker() + isMemberCompletion := isMemberCompletionKind(data.completionKind) // Tracks unique names. // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; // true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports. @@ -944,7 +945,7 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( var sortText sortText if isDeprecated(symbol, typeChecker) { - sortText = deprecateSortText(originalSortText) + sortText = DeprecateSortText(originalSortText) } else { sortText = originalSortText } @@ -963,12 +964,13 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( compilerOptions, preferences, clientOptions, + isMemberCompletion, ) if entry == nil { continue } - /** True for locals; false for globals, module exports from other files, `this.` completions. */ + // True for locals; false for globals, module exports from other files, `this.` completions. shouldShadowLaterSymbols := (origin == nil || originIsTypeOnlyAlias(origin)) && !(symbol.Parent == nil && !core.Some(symbol.Declarations, func(d *ast.Node) bool { return ast.GetSourceFileOfNode(d) == file })) @@ -1028,6 +1030,7 @@ func (l *LanguageService) createCompletionItem( compilerOptions *core.CompilerOptions, preferences *UserPreferences, clientOptions *lsproto.CompletionClientCapabilities, + isMemberCompletion bool, ) *lsproto.CompletionItem { contextToken := data.contextToken var insertText string @@ -1250,6 +1253,8 @@ func (l *LanguageService) createCompletionItem( } } + // Commit characters + elementKind := getSymbolKind(typeChecker, symbol, data.location) kind := getCompletionsSymbolKind(elementKind) var commitCharacters *[]string @@ -1262,10 +1267,77 @@ func (l *LanguageService) createCompletionItem( // Otherwise use the completion list default. } + // Text edit + + var textEdit *lsproto.TextEditOrInsertReplaceEdit + if replacementSpan != nil { + textEdit = &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: core.IfElse(insertText == "", name, insertText), + Range: *replacementSpan, + }, + } + } else { + // Ported from vscode ts extension. + optionalReplacementSpan := getOptionalReplacementSpan(data.location, file) + if optionalReplacementSpan != nil && ptrIsTrue(clientOptions.CompletionItem.InsertReplaceSupport) { + insertRange := l.createLspRangeFromBounds(optionalReplacementSpan.Pos(), position, file) + replaceRange := l.createLspRangeFromBounds(optionalReplacementSpan.Pos(), optionalReplacementSpan.End(), file) + textEdit = &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: core.IfElse(insertText == "", name, insertText), + Insert: *insertRange, + Replace: *replaceRange, + }, + } + } + } + + // Filter text + + // Ported from vscode ts extension. + wordRange, wordStart := getWordRange(file, position) + if filterText == "" { + filterText = getFilterText(file, position, insertText, name, isMemberCompletion, isSnippet, wordStart) + } + if isMemberCompletion && !isSnippet { + accessorRange, accessorText := getDotAccessorContext(file, position) + if accessorText != "" { + filterText = accessorText + core.IfElse(insertText != "", insertText, name) + if textEdit == nil { + insertText = filterText + if wordRange != nil && ptrIsTrue(clientOptions.CompletionItem.InsertReplaceSupport) { + textEdit = &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: insertText, + Insert: *l.createLspRangeFromBounds( + accessorRange.Pos(), + accessorRange.End(), + file), + Replace: *l.createLspRangeFromBounds( + min(accessorRange.Pos(), wordRange.Pos()), + accessorRange.End(), + file), + }, + } + } else { + textEdit = &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: insertText, + Range: *l.createLspRangeFromBounds(accessorRange.Pos(), accessorRange.End(), file), + }, + } + } + } + } + } + + // Adjustements based on kind modifiers. + kindModifiers := getSymbolModifiers(typeChecker, symbol) var tags *[]lsproto.CompletionItemTag var detail *string - // Copied from vscode ts extension. + // Copied from vscode ts extension: `MyCompletionItem.constructor`. if kindModifiers.Has(ScriptElementKindModifierOptional) { if insertText == "" { insertText = name @@ -1302,17 +1374,6 @@ func (l *LanguageService) createCompletionItem( insertTextFormat = ptrTo(lsproto.InsertTextFormatPlainText) } - var textEdit *lsproto.TextEditOrInsertReplaceEdit - if replacementSpan != nil { - textEdit = &lsproto.TextEditOrInsertReplaceEdit{ - TextEdit: &lsproto.TextEdit{ - NewText: core.IfElse(insertText == "", name, insertText), - Range: *replacementSpan, - }, - } - } - // !!! adjust text edit like vscode does when Strada's `isMemberCompletion` is true - return &lsproto.CompletionItem{ Label: name, LabelDetails: labelDetails, @@ -1340,6 +1401,118 @@ func isRecommendedCompletionMatch(localSymbol *ast.Symbol, recommendedCompletion localSymbol.Flags&ast.SymbolFlagsExportValue != 0 && typeChecker.GetExportSymbolOfSymbol(localSymbol) == recommendedCompletion } +// Ported from vscode's `USUAL_WORD_SEPARATORS`. +var wordSeparators = core.NewSetFromItems( + '`', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '=', '+', '[', '{', ']', '}', '\\', '|', + ';', ':', '\'', '"', ',', '.', '<', '>', '/', '?', +) + +// Finds the range of the word that ends at the given position. +// e.g. for "abc def.ghi|jkl", the word range is "ghi" and the word start is 'g'. +func getWordRange(sourceFile *ast.SourceFile, position int) (wordRange *core.TextRange, wordStart rune) { + // !!! Port other case of vscode's `DEFAULT_WORD_REGEXP` that covers words that start like numbers, e.g. -123.456abcd. + text := sourceFile.Text()[:position] + totalSize := 0 + var firstRune rune + for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-size]) { + if wordSeparators.Has(r) || unicode.IsSpace(r) { + break + } + totalSize += size + firstRune = r + } + // If word starts with `@`, disregard this first character. + if firstRune == '@' { + totalSize -= 1 + firstRune, _ = utf8.DecodeRuneInString(text[len(text)-totalSize:]) + } + if totalSize == 0 { + return nil, firstRune + } + textRange := core.NewTextRange(position-totalSize, position) + return &textRange, firstRune +} + +// Ported from vscode ts extension: `getFilterText`. +func getFilterText( + file *ast.SourceFile, + position int, + insertText string, + label string, + isMemberCompletion bool, + isSnippet bool, + wordStart rune, +) string { + // Private field completion. + if strings.HasPrefix(label, "#") { + // !!! document theses cases + if insertText != "" { + if strings.HasPrefix(insertText, "this.#") { + if wordStart == '#' { + return insertText + } else { + return strings.TrimPrefix(insertText, "this.#") + } + } + } else { + if wordStart == '#' { + return "" + } else { + return strings.TrimPrefix(label, "#") + } + } + } + + // For `this.` completions, generally don't set the filter text since we don't want them to be overly prioritized. microsoft/vscode#74164 + if strings.HasPrefix(insertText, "this.") { + return "" + } + + // Handle the case: + // ``` + // const xyz = { 'ab c': 1 }; + // xyz.ab| + // ``` + // In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of + // the bracketed insert text. + if strings.HasPrefix(insertText, "[") { + if strings.HasPrefix(insertText, `['`) && strings.HasSuffix(insertText, `']`) { + return "." + strings.TrimPrefix(strings.TrimSuffix(insertText, `']`), `['`) + } + if strings.HasPrefix(insertText, `["`) && strings.HasSuffix(insertText, `"]`) { + return "." + strings.TrimPrefix(strings.TrimSuffix(insertText, `"]`), `["`) + } + return insertText + } + + // In all other cases, fall back to using the insertText. + return insertText +} + +// Ported from vscode's `provideCompletionItems`. +func getDotAccessorContext(file *ast.SourceFile, position int) (acessorRange *core.TextRange, accessorText string) { + text := file.Text()[:position] + totalSize := 0 + for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-size]) { + if !unicode.IsSpace(r) { + break + } + totalSize += size + text = text[:len(text)-size] + } + if strings.HasSuffix(text, "?.") { + totalSize += 2 + newRange := core.NewTextRange(position-totalSize, position) + return &newRange, file.Text()[position-totalSize : position] + } + if strings.HasSuffix(text, ".") { + totalSize += 1 + newRange := core.NewTextRange(position-totalSize, position) + return &newRange, file.Text()[position-totalSize : position] + } + return nil, "" +} + func strPtrTo(v string) *string { if v == "" { return nil @@ -2319,3 +2492,19 @@ func getJSCompletionEntries( } return sortedEntries } + +func getOptionalReplacementSpan(location *ast.Node, file *ast.SourceFile) *core.TextRange { + // StringLiteralLike locations are handled separately in stringCompletions.ts + if location != nil && location.Kind == ast.KindIdentifier { + start := astnav.GetStartOfNode(location, file, false /*includeJSDoc*/) + textRange := core.NewTextRange(start, location.End()) + return &textRange + } + return nil +} + +func isMemberCompletionKind(kind CompletionKind) bool { + return kind == CompletionKindObjectPropertyDeclaration || + kind == CompletionKindMemberLike || + kind == CompletionKindPropertyAccess +} diff --git a/internal/ls/completions_test.go b/internal/ls/completions_test.go index 883844835f..31675cbf43 100644 --- a/internal/ls/completions_test.go +++ b/internal/ls/completions_test.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil/lstestutil" "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" "gotest.tools/v3/assert" ) @@ -15,11 +16,12 @@ var defaultCommitCharacters = []string{".", ",", ";"} type testCase struct { name string - content string - position int - expected *lsproto.CompletionList + files map[string]string + expected map[string]*lsproto.CompletionList } +const mainFileName = "/index.ts" + func TestCompletions(t *testing.T) { t.Parallel() if !bundled.Embedded { @@ -27,95 +29,763 @@ func TestCompletions(t *testing.T) { // Just skip this for now. t.Skip("bundled files are not embedded") } + + itemDefaults := &lsproto.CompletionItemDefaults{ + CommitCharacters: &defaultCommitCharacters, + } + insertTextFormatPlainText := ptrTo(lsproto.InsertTextFormatPlainText) + sortTextLocationPriority := ptrTo(string(ls.SortTextLocationPriority)) + sortTextLocalDeclarationPriority := ptrTo(string(ls.SortTextLocalDeclarationPriority)) + sortTextDeprecatedLocationPriority := ptrTo(string(ls.DeprecateSortText(ls.SortTextLocationPriority))) + fieldKind := ptrTo(lsproto.CompletionItemKindField) + methodKind := ptrTo(lsproto.CompletionItemKindMethod) + functionKind := ptrTo(lsproto.CompletionItemKindFunction) + variableKind := ptrTo(lsproto.CompletionItemKindVariable) + + stringMembers := []*lsproto.CompletionItem{ + {Label: "charAt", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "charCodeAt", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "concat", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "indexOf", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "lastIndexOf", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "length", Kind: fieldKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "localeCompare", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "match", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "replace", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "search", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "slice", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "split", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "substring", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "toLocaleLowerCase", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "toLocaleUpperCase", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "toLowerCase", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "toString", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "toUpperCase", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "trim", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "valueOf", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "substr", Kind: methodKind, SortText: sortTextDeprecatedLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + } + + arrayMembers := []*lsproto.CompletionItem{ + {Label: "concat", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "every", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "filter", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "forEach", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "indexOf", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "join", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "lastIndexOf", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "length", Kind: fieldKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "map", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "pop", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "push", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "reduce", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "reduceRight", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "reverse", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "shift", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "slice", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "some", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "sort", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "splice", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "toLocaleString", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "toString", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + {Label: "unshift", Kind: methodKind, SortText: sortTextLocationPriority, InsertTextFormat: insertTextFormatPlainText}, + } + testCases := []testCase{ { name: "basicInterfaceMembers", - content: `export {}; + files: map[string]string{ + mainFileName: `export {}; interface Point { x: number; y: number; } declare const p: Point; -p.`, - position: 87, - expected: &lsproto.CompletionList{ - IsIncomplete: false, - ItemDefaults: &lsproto.CompletionItemDefaults{ - CommitCharacters: &defaultCommitCharacters, - }, - Items: []*lsproto.CompletionItem{ - { - Label: "x", - Kind: ptrTo(lsproto.CompletionItemKindField), - SortText: ptrTo(string(ls.SortTextLocationPriority)), - InsertTextFormat: ptrTo(lsproto.InsertTextFormatPlainText), +p./*a*/`, + }, + expected: map[string]*lsproto.CompletionList{ + "a": { + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "x", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".x"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "x", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 2}, + End: lsproto.Position{Line: 6, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 2}, + End: lsproto.Position{Line: 6, Character: 2}, + }, + }, + }, + }, + { + Label: "y", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".y"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "y", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 2}, + End: lsproto.Position{Line: 6, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 2}, + End: lsproto.Position{Line: 6, Character: 2}, + }, + }, + }, + }, }, - { - Label: "y", - Kind: ptrTo(lsproto.CompletionItemKindField), - SortText: ptrTo(string(ls.SortTextLocationPriority)), - InsertTextFormat: ptrTo(lsproto.InsertTextFormatPlainText), + }, + }, + }, + { + name: "basicInterfaceMembersOptional", + files: map[string]string{ + "/tsconfig.json": `{ "compilerOptions": { "strict": true } }`, + mainFileName: `export {}; +interface Point { + x: number; + y: number; +} +declare const p: Point | undefined; +p./*a*/`, + }, + expected: map[string]*lsproto.CompletionList{ + "a": { + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "x", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".?.x"), + InsertText: ptrTo("?.x"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: "?.x", + Range: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 1}, + End: lsproto.Position{Line: 6, Character: 2}, + }, + }, + }, + }, + { + Label: "y", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".?.y"), + InsertText: ptrTo("?.y"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: "?.y", + Range: lsproto.Range{ + Start: lsproto.Position{Line: 6, Character: 1}, + End: lsproto.Position{Line: 6, Character: 2}, + }, + }, + }, + }, }, }, }, }, { name: "objectLiteralType", - content: `export {}; + files: map[string]string{ + mainFileName: `export {}; let x = { foo: 123 }; -x.`, - position: 35, - expected: &lsproto.CompletionList{ - IsIncomplete: false, - ItemDefaults: &lsproto.CompletionItemDefaults{ - CommitCharacters: &defaultCommitCharacters, +x./*a*/`, + }, + expected: map[string]*lsproto.CompletionList{ + "a": { + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "foo", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".foo"), + InsertTextFormat: ptrTo(lsproto.InsertTextFormatPlainText), + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "foo", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 2, Character: 2}, + End: lsproto.Position{Line: 2, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 2, Character: 2}, + End: lsproto.Position{Line: 2, Character: 2}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "basicClassMembers", + files: map[string]string{ + mainFileName: ` +class n { + constructor (public x: number, public y: number, private z: string) { } +} +var t = new n(0, 1, '');t./*a*/`, + }, + expected: map[string]*lsproto.CompletionList{ + "a": { + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "x", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".x"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "x", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 3, Character: 26}, + End: lsproto.Position{Line: 3, Character: 26}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 3, Character: 26}, + End: lsproto.Position{Line: 3, Character: 26}, + }, + }, + }, + }, + { + Label: "y", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".y"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "y", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 3, Character: 26}, + End: lsproto.Position{Line: 3, Character: 26}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 3, Character: 26}, + End: lsproto.Position{Line: 3, Character: 26}, + }, + }, + }, + }, + }, }, - Items: []*lsproto.CompletionItem{ - { - Label: "foo", - Kind: ptrTo(lsproto.CompletionItemKindField), - SortText: ptrTo(string(ls.SortTextLocationPriority)), - InsertTextFormat: ptrTo(lsproto.InsertTextFormatPlainText), + }, + }, + { + name: "cloduleAsBaseClass", + files: map[string]string{ + mainFileName: ` +class A { + constructor(x: number) { } + foo() { } + static bar() { } +} + +module A { + export var x = 1; + export function baz() { } +} + +class D extends A { + constructor() { + super(1); + } + foo2() { } + static bar2() { } +} + +D./*a*/`, + }, + expected: map[string]*lsproto.CompletionList{ + "a": { + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ // !!! `funcionMembersPlus` + { + Label: "bar", + Kind: methodKind, + SortText: sortTextLocalDeclarationPriority, + FilterText: ptrTo(".bar"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "bar", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "bar2", + Kind: methodKind, + SortText: sortTextLocalDeclarationPriority, + FilterText: ptrTo(".bar2"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "bar2", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "apply", + Kind: methodKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".apply"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "apply", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "arguments", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".arguments"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "arguments", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "baz", + Kind: functionKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".baz"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "baz", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "bind", + Kind: methodKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".bind"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "bind", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "call", + Kind: methodKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".call"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "call", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "caller", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".caller"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "caller", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "length", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".length"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "length", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "prototype", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".prototype"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "prototype", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "toString", + Kind: methodKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".toString"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "toString", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + { + Label: "x", + Kind: variableKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".x"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "x", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 19, Character: 2}, + End: lsproto.Position{Line: 19, Character: 2}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "lambdaThisMembers", + files: map[string]string{ + mainFileName: `class Foo { + a: number; + b() { + var x = () => { + this./**/; + } + } +}`, + }, + expected: map[string]*lsproto.CompletionList{ + "": { + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: []*lsproto.CompletionItem{ + { + Label: "a", + Kind: fieldKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".a"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "a", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 4, Character: 17}, + End: lsproto.Position{Line: 4, Character: 17}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 4, Character: 17}, + End: lsproto.Position{Line: 4, Character: 17}, + }, + }, + }, + }, + { + Label: "b", + Kind: methodKind, + SortText: sortTextLocationPriority, + FilterText: ptrTo(".b"), + InsertTextFormat: insertTextFormatPlainText, + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: "b", + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 4, Character: 17}, + End: lsproto.Position{Line: 4, Character: 17}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 4, Character: 17}, + End: lsproto.Position{Line: 4, Character: 17}, + }, + }, + }, + }, }, }, }, }, + { + name: "memberCompletionInForEach1", + files: map[string]string{ + mainFileName: `var x: string[] = []; +x.forEach(function (y) { y./*1*/`, + }, + expected: map[string]*lsproto.CompletionList{ + "1": { + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: core.Map(stringMembers, func(basicItem *lsproto.CompletionItem) *lsproto.CompletionItem { + item := *basicItem + item.FilterText = ptrTo("." + item.Label) + item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: item.Label, + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 1, Character: 27}, + End: lsproto.Position{Line: 1, Character: 27}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 1, Character: 27}, + End: lsproto.Position{Line: 1, Character: 27}, + }, + }, + } + return &item + }), + }, + }, + }, + { + name: "completionsTuple", + files: map[string]string{ + mainFileName: `declare const x: [number, number]; +x./**/;`, + }, + expected: map[string]*lsproto.CompletionList{ + "": { + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: append([]*lsproto.CompletionItem{ + { + Label: "0", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertText: ptrTo("[0]"), + InsertTextFormat: insertTextFormatPlainText, + FilterText: ptrTo(".[0]"), + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: "[0]", + Range: lsproto.Range{ + Start: lsproto.Position{Line: 1, Character: 1}, + End: lsproto.Position{Line: 1, Character: 2}, + }, + }, + }, + }, + { + Label: "1", + Kind: fieldKind, + SortText: sortTextLocationPriority, + InsertText: ptrTo("[1]"), + InsertTextFormat: insertTextFormatPlainText, + FilterText: ptrTo(".[1]"), + TextEdit: &lsproto.TextEditOrInsertReplaceEdit{ + TextEdit: &lsproto.TextEdit{ + NewText: "[1]", + Range: lsproto.Range{ + Start: lsproto.Position{Line: 1, Character: 1}, + End: lsproto.Position{Line: 1, Character: 2}, + }, + }, + }, + }, + }, core.Map(arrayMembers, func(basicItem *lsproto.CompletionItem) *lsproto.CompletionItem { + item := *basicItem + item.FilterText = ptrTo("." + item.Label) + item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{ + InsertReplaceEdit: &lsproto.InsertReplaceEdit{ + NewText: item.Label, + Insert: lsproto.Range{ + Start: lsproto.Position{Line: 1, Character: 2}, + End: lsproto.Position{Line: 1, Character: 2}, + }, + Replace: lsproto.Range{ + Start: lsproto.Position{Line: 1, Character: 2}, + End: lsproto.Position{Line: 1, Character: 2}, + }, + }, + } + return &item + })...), + }, + }, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { t.Parallel() - runTest(t, testCase.content, testCase.position, testCase.expected) + runTest(t, testCase.files, testCase.expected) }) } } -func runTest(t *testing.T, content string, position int, expected *lsproto.CompletionList) { - files := map[string]string{ - "/index.ts": content, +func runTest(t *testing.T, files map[string]string, expected map[string]*lsproto.CompletionList) { + parsedFiles := make(map[string]string) + var markerPositions map[string]*lstestutil.Marker + for fileName, content := range files { + if fileName == mainFileName { + testData := lstestutil.ParseTestData("", content, fileName) + markerPositions = testData.MarkerPositions + parsedFiles[fileName] = testData.Files[0].Content // !!! Assumes no usage of @filename + } else { + parsedFiles[fileName] = content + } } - languageService := createLanguageService("/index.ts", files) + languageService := createLanguageService(mainFileName, parsedFiles) context := &lsproto.CompletionContext{ TriggerKind: lsproto.CompletionTriggerKindInvoked, } + ptrTrue := ptrTo(true) capabilities := &lsproto.CompletionClientCapabilities{ CompletionItem: &lsproto.ClientCompletionItemOptions{ - SnippetSupport: ptrTo(true), - CommitCharactersSupport: ptrTo(true), - PreselectSupport: ptrTo(true), - LabelDetailsSupport: ptrTo(true), + SnippetSupport: ptrTrue, + CommitCharactersSupport: ptrTrue, + PreselectSupport: ptrTrue, + LabelDetailsSupport: ptrTrue, + InsertReplaceSupport: ptrTrue, }, CompletionList: &lsproto.CompletionListCapabilities{ ItemDefaults: &[]string{"commitCharacters"}, }, } preferences := &ls.UserPreferences{} - completionList := languageService.ProvideCompletion( - "/index.ts", - position, - context, - capabilities, - preferences) - assert.DeepEqual(t, completionList, expected) + + for markerName, expectedResult := range expected { + marker, ok := markerPositions[markerName] + if !ok { + t.Fatalf("No marker found for '%s'", markerName) + } + completionList := languageService.ProvideCompletion( + "/index.ts", + marker.Position, + context, + capabilities, + preferences) + assert.DeepEqual(t, completionList, expectedResult) + } } func createLanguageService(fileName string, files map[string]string) *ls.LanguageService { diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index 60f0b830a1..2fc19e4549 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -66,7 +66,7 @@ func (l *LanguageService) tryGetProgramAndFile(fileName string) (*compiler.Progr func (l *LanguageService) getProgramAndFile(fileName string) (*compiler.Program, *ast.SourceFile) { program, file := l.tryGetProgramAndFile(fileName) if file == nil { - panic("file not found") + panic("file not found: " + fileName) } return program, file } diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 6c642513a3..719588ae09 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -82,23 +82,73 @@ func assertHasRealPosition(node *ast.Node) { } } -// !!! -func findChildOfKind(node *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) *ast.Node { +func findChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) *ast.Node { + lastNodePos := containingNode.Pos() + scanner := scanner.GetScannerForSourceFile(sourceFile, lastNodePos) + + var foundChild *ast.Node + visitNode := func(node *ast.Node) bool { + if node == nil || node.Flags&ast.NodeFlagsReparsed != 0 { + return false + } + // Look for child in preceding tokens. + startPos := lastNodePos + for startPos < node.Pos() { + tokenKind := scanner.Token() + tokenFullStart := scanner.TokenFullStart() + tokenEnd := scanner.TokenEnd() + token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, containingNode) + if tokenKind == kind { + foundChild = token + return true + } + startPos = tokenEnd + scanner.Scan() + } + if node.Kind == kind { + foundChild = node + return true + } + + lastNodePos = node.End() + scanner.ResetPos(lastNodePos) + return false + } + + ast.ForEachChildAndJSDoc(containingNode, sourceFile, visitNode) + + if foundChild != nil { + return foundChild + } + + // Look for child in trailing tokens. + startPos := lastNodePos + for startPos < containingNode.End() { + tokenKind := scanner.Token() + tokenFullStart := scanner.TokenFullStart() + tokenEnd := scanner.TokenEnd() + token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, containingNode) + if tokenKind == kind { + return token + } + startPos = tokenEnd + scanner.Scan() + } return nil } -// !!! +// !!! signature help type PossibleTypeArgumentInfo struct { called *ast.IdentifierNode nTypeArguments int } -// !!! +// !!! signature help func getPossibleTypeArgumentsInfo(tokenIn *ast.Node, sourceFile *ast.SourceFile) *PossibleTypeArgumentInfo { return nil } -// !!! +// !!! signature help func getPossibleGenericSignatures(called *ast.Expression, typeArgumentCount int, checker *checker.Checker) []*checker.Signature { return nil } diff --git a/internal/testutil/lstestutil/lstestutil.go b/internal/testutil/lstestutil/lstestutil.go new file mode 100644 index 0000000000..fc81641246 --- /dev/null +++ b/internal/testutil/lstestutil/lstestutil.go @@ -0,0 +1,187 @@ +package lstestutil + +import ( + "fmt" + "strings" + + "github.com/microsoft/typescript-go/internal/core" +) + +type markerRange struct { + core.TextRange + filename string + position int + data string +} + +type Marker struct { + Filename string + Position int + Name string +} + +type TestData struct { + Files []*TestFileInfo + MarkerPositions map[string]*Marker + //markers []*Marker + /** + * Inserted in source files by surrounding desired text + * in a range with `[|` and `|]`. For example, + * + * [|text in range|] + * + * is a range with `text in range` "selected". + */ + Ranges markerRange +} + +func ParseTestData(basePath string, contents string, fileName string) TestData { + // List of all the subfiles we've parsed out + var files []*TestFileInfo + + // Split up the input file by line + lines := strings.Split(contents, "\n") + currentFileContent := "" + + for _, line := range lines { + if len(line) > 0 && line[len(line)-1] == '\r' { + line = line[:len(line)-1] + } + if currentFileContent == "" { + currentFileContent = line + } else { + currentFileContent += "\n" + line + } + } + + if currentFileContent == "" { + return TestData{} + } + markerPositions := make(map[string]*Marker) + markers := []*Marker{} + + // If we have multiple files, then parseFileContent needs to be called for each file. + // This will be achieved by creating a `nextFile()` func that will call `parseFileContent()` for each file. + testFileInfo := parseFileContent(currentFileContent, fileName, markerPositions, &markers) + files = append(files, testFileInfo) + + return TestData{ + Files: files, + MarkerPositions: markerPositions, + // markers: markers, + Ranges: markerRange{}, + } +} + +type locationInformation struct { + position int + sourcePosition int + sourceLine int + sourceColumn int +} + +type TestFileInfo struct { // for FourSlashFile + Filename string + // The contents of the file (with markers, etc stripped out) + Content string +} + +func parseFileContent(content string, filename string, markerMap map[string]*Marker, markers *[]*Marker) *TestFileInfo { + // !!! chompLeadingSpace + // !!! validate characters in markers + // Any slash-star comment with a character not in this string is not a marker. + // const validMarkerChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$1234567890_" + + /// The file content (minus metacharacters) so far + output := "" + + /// The total number of metacharacters removed from the file (so far) + difference := 0 + + /// Current position data + line := 1 + column := 1 + + /// The current marker (or maybe multi-line comment?) we're parsing, possibly + var openMarker locationInformation + + /// The latest position of the start of an unflushed plain text area + lastNormalCharPosition := 0 + + flush := func(lastSafeCharIndex int) { + if lastSafeCharIndex != -1 { + output = output + content[lastNormalCharPosition:lastSafeCharIndex] + } else { + output = output + content[lastNormalCharPosition:] + } + } + + previousCharacter := content[0] + for i := 1; i < len(content); i++ { + currentCharacter := content[i] + if previousCharacter == '/' && currentCharacter == '*' { + // found a possible marker start + openMarker = locationInformation{ + position: (i - 1) - difference, + sourcePosition: i - 1, + sourceLine: line, + sourceColumn: column, + } + } + if previousCharacter == '*' && currentCharacter == '/' { + // Record the marker + // start + 2 to ignore the */, -1 on the end to ignore the * (/ is next) + markerNameText := strings.TrimSpace(content[openMarker.sourcePosition+2 : i-1]) + recordMarker(filename, openMarker, markerNameText, markerMap, markers) + + flush(openMarker.sourcePosition) + lastNormalCharPosition = i + 1 + difference += i + 1 - openMarker.sourcePosition + + // Set the current start to point to the end of the current marker to ignore its text + openMarker = locationInformation{} + } + if currentCharacter == '\n' && previousCharacter == '\r' { + // Ignore trailing \n after \r + continue + } else if currentCharacter == '\n' || currentCharacter == '\r' { + line++ + column = 1 + continue + } + + column++ + previousCharacter = currentCharacter + } + + // Add the remaining text + flush(-1) + + return &TestFileInfo{ + Content: output, + Filename: filename, + } +} + +func recordMarker( + filename string, + location locationInformation, + name string, + markerMap map[string]*Marker, + markers *[]*Marker, +) *Marker { + // Record the marker + marker := &Marker{ + Filename: filename, + Position: location.position, + Name: name, + } + // Verify markers for uniqueness + if _, ok := markerMap[name]; ok { + fmt.Printf("Duplicate marker name: %s\n", name) // tbd print error msg + } else { + markerMap[name] = marker + (*markers) = append(*markers, marker) + } + return marker +}