diff --git a/docs/content/docs/reference/verbs.md b/docs/content/docs/reference/verbs.md index 93c8f37fa5..eedcc13bf5 100644 --- a/docs/content/docs/reference/verbs.md +++ b/docs/content/docs/reference/verbs.md @@ -15,7 +15,7 @@ top = false ## Defining Verbs -To declare a Verb, write a normal Go function with the following signature,annotated with the Go [comment directive](https://tip.golang.org/doc/comment#syntax) `//ftl:verb`: +To declare a Verb, write a normal Go function with the following signature, annotated with the Go [comment directive](https://tip.golang.org/doc/comment#syntax) `//ftl:verb`: ```go //ftl:verb diff --git a/lsp/completion.go b/lsp/completion.go index b337da7220..ed5d950916 100644 --- a/lsp/completion.go +++ b/lsp/completion.go @@ -70,19 +70,28 @@ var completionItems = []protocol.CompletionItem{ completionItem("ftl:fsm", "Model a FSM", fsmCompletionDocs), } +// Track which directives are //ftl: prefixed, so the we can autocomplete them via `/`. +// This is built at init time and does not change during runtime. +var directiveItems = map[string]bool{} + func completionItem(label, detail, markdown string) protocol.CompletionItem { snippetKind := protocol.CompletionItemKindSnippet insertTextFormat := protocol.InsertTextFormatSnippet parts := strings.Split(markdown, "---") if len(parts) != 2 { - panic("invalid markdown. must contain exactly one '---' to separate completion docs from insert text") + panic(fmt.Sprintf("completion item %q: invalid markdown. must contain exactly one '---' to separate completion docs from insert text", label)) } insertText := strings.TrimSpace(parts[1]) // Warn if we see two spaces in the insert text. if strings.Contains(insertText, " ") { - panic(fmt.Sprintf("completion item %q contains two spaces in the insert text. Use tabs instead!", label)) + panic(fmt.Sprintf("completion item %q: contains two spaces in the insert text. Use tabs instead!", label)) + } + + // If there is a `//ftl:` this can be autocompleted when the user types `/`. + if strings.Contains(insertText, "//ftl:") { + directiveItems[label] = true } return protocol.CompletionItem{ @@ -108,25 +117,70 @@ func (s *Server) textDocumentCompletion() protocol.TextDocumentCompletionFunc { return nil, nil } + // Line and Character are 0-based, however the cursor can be after the last character in the line. line := int(position.Line) if line >= len(doc.lines) { return nil, nil } - lineContent := doc.lines[line] - character := int(position.Character - 1) + character := int(position.Character) if character > len(lineContent) { character = len(lineContent) } - prefix := lineContent[character:] + // Currently all completions are in global scope, so the completion must be triggered at the beginning of the line. + // To do this, check to the start of the line and if there is any whitespace, it is not completing a whole word from the start. + // We also want to check that the cursor is at the end of the line so we dont let stray chars shoved at the end of the completion. + isAtEOL := character == len(lineContent) + if !isAtEOL { + return nil, nil + } + + // Is not completing from the start of the line. + if strings.ContainsAny(lineContent, " \t") { + return nil, nil + } + + // If there is a single `/` at the start of the line, we can autocomplete directives. eg `/f`. + // This is a hint to the user that these are ftl directives. + // Note that what I can tell, VSCode won't trigger completion on and after `//` so we can only complete on half of a comment. + isSlashed := strings.HasPrefix(lineContent, "/") + if isSlashed { + lineContent = strings.TrimPrefix(lineContent, "/") + } - // Filter completion items based on the prefix + // Filter completion items based on the line content and if it is a directive. var filteredItems []protocol.CompletionItem for _, item := range completionItems { - if strings.HasPrefix(item.Label, prefix) || strings.Contains(item.Label, prefix) { - filteredItems = append(filteredItems, item) + if !strings.Contains(item.Label, lineContent) { + continue + } + + if isSlashed && !directiveItems[item.Label] { + continue + } + + if isSlashed { + // Remove that / from the start of the line, so that the completion doesn't have `///`. + // VSCode doesn't seem to want to remove the `/` for us. + item.AdditionalTextEdits = []protocol.TextEdit{ + { + Range: protocol.Range{ + Start: protocol.Position{ + Line: uint32(line), + Character: 0, + }, + End: protocol.Position{ + Line: uint32(line), + Character: 1, + }, + }, + NewText: "", + }, + } } + + filteredItems = append(filteredItems, item) } return &protocol.CompletionList{ diff --git a/lsp/markdown/completion/configDeclare.md b/lsp/markdown/completion/configDeclare.md index 85d608beda..12945583bc 100644 --- a/lsp/markdown/completion/configDeclare.md +++ b/lsp/markdown/completion/configDeclare.md @@ -8,4 +8,5 @@ var defaultUser = ftl.Config[string]("defaultUser") See https://tbd54566975.github.io/ftl/docs/reference/secretsconfig/ --- + var ${1:configVar} = ftl.Config[${2:Type}]("${1:configVar}") diff --git a/lsp/markdown/completion/enumType.md b/lsp/markdown/completion/enumType.md index f584446fbc..f28f6426df 100644 --- a/lsp/markdown/completion/enumType.md +++ b/lsp/markdown/completion/enumType.md @@ -15,6 +15,7 @@ func (Dog) animal() {} See https://tbd54566975.github.io/ftl/docs/reference/types/ --- + //ftl:enum type ${1:Type} interface { ${2:interface}() } diff --git a/lsp/markdown/completion/enumValue.md b/lsp/markdown/completion/enumValue.md index b7db62aefe..86cc0868d4 100644 --- a/lsp/markdown/completion/enumValue.md +++ b/lsp/markdown/completion/enumValue.md @@ -15,6 +15,7 @@ const ( See https://tbd54566975.github.io/ftl/docs/reference/types/ --- + //ftl:enum type ${1:Enum} string diff --git a/lsp/markdown/completion/ingress.md b/lsp/markdown/completion/ingress.md index 34564656a6..f7b24af54e 100644 --- a/lsp/markdown/completion/ingress.md +++ b/lsp/markdown/completion/ingress.md @@ -23,17 +23,16 @@ func Get(ctx context.Context, req builtin.HttpRequest[GetRequest]) (builtin.Http See https://tbd54566975.github.io/ftl/docs/reference/ingress/ --- + type ${1:Func}Request struct { - ${2:Field} ${3:Type} `json:"${4:field}"` } type ${1:Func}Response struct { - ${5:Field} ${6:Type} `json:"${7:field}"` } -//ftl:ingress ${8:GET} ${9:/url/path} +//ftl:ingress ${2:GET} ${3:/url/path} func ${1:Func}(ctx context.Context, req builtin.HttpRequest[${1:Func}Request]) (builtin.HttpResponse[${1:Func}Response, string], error) { - ${7:// TODO: Implement} + ${4:// TODO: Implement} return builtin.HttpResponse[${1:Func}Response, string]{ Status: 200, Body: ftl.Some(${1:Func}Response{}), diff --git a/lsp/markdown/completion/pubSubSink.md b/lsp/markdown/completion/pubSubSink.md index 1ade529ddf..13c5847662 100644 --- a/lsp/markdown/completion/pubSubSink.md +++ b/lsp/markdown/completion/pubSubSink.md @@ -9,6 +9,7 @@ func SendInvoiceEmail(ctx context.Context, in Invoice) error { See https://tbd54566975.github.io/ftl/docs/reference/pubsub/ --- + //ftl:subscribe ${1:subscriptionName} func ${2:FunctionName}(ctx context.Context, in ${3:Type}) error { ${4:// TODO: Implement} diff --git a/lsp/markdown/completion/pubSubSubscription.md b/lsp/markdown/completion/pubSubSubscription.md index bc48ba6d8c..09a5aeb3f3 100644 --- a/lsp/markdown/completion/pubSubSubscription.md +++ b/lsp/markdown/completion/pubSubSubscription.md @@ -6,4 +6,5 @@ var _ = ftl.Subscription(invoicesTopic, "emailInvoices") See https://tbd54566975.github.io/ftl/docs/reference/pubsub/ --- + var _ = ftl.Subscription(${1:topicVar}, "${2:subscriptionName}") diff --git a/lsp/markdown/completion/pubSubTopic.md b/lsp/markdown/completion/pubSubTopic.md index 4303364fe3..25507b7b26 100644 --- a/lsp/markdown/completion/pubSubTopic.md +++ b/lsp/markdown/completion/pubSubTopic.md @@ -6,4 +6,5 @@ var invoicesTopic = ftl.Topic[Invoice]("invoices") See https://tbd54566975.github.io/ftl/docs/reference/pubsub/ --- + var ${1:topicVar} = ftl.Topic[${2:Type}]("${1:topicName}") diff --git a/lsp/markdown/completion/retry.md b/lsp/markdown/completion/retry.md index e3bfc7b596..365c5f09e4 100644 --- a/lsp/markdown/completion/retry.md +++ b/lsp/markdown/completion/retry.md @@ -8,4 +8,5 @@ Any verb called asynchronously (specifically, PubSub subscribers and FSM states) See https://tbd54566975.github.io/ftl/docs/reference/retries/ --- -//ftl:retry ${1:attempts} ${2:minBackoff} ${3:maxBackoff} \ No newline at end of file + +//ftl:retry ${1:attempts} ${2:minBackoff} ${3:maxBackoff} diff --git a/lsp/markdown/completion/secretDeclare.md b/lsp/markdown/completion/secretDeclare.md index 3c789a1253..cd4d81feeb 100644 --- a/lsp/markdown/completion/secretDeclare.md +++ b/lsp/markdown/completion/secretDeclare.md @@ -8,4 +8,5 @@ var apiKey = ftl.Secret[string]("apiKey") See https://tbd54566975.github.io/ftl/docs/reference/secretsconfig/ --- + var ${1:secretVar} = ftl.Secret[${2:Type}]("${1:secretVar}") diff --git a/lsp/markdown/completion/typeAlias.md b/lsp/markdown/completion/typeAlias.md index e92ce9f6e1..805436a8ac 100644 --- a/lsp/markdown/completion/typeAlias.md +++ b/lsp/markdown/completion/typeAlias.md @@ -9,5 +9,6 @@ type UserID string See https://tbd54566975.github.io/ftl/docs/reference/types/ --- + //ftl:typealias type ${1:Alias} ${2:Type} diff --git a/lsp/markdown/completion/verb.md b/lsp/markdown/completion/verb.md index 8ab1200d9a..df2250eb5b 100644 --- a/lsp/markdown/completion/verb.md +++ b/lsp/markdown/completion/verb.md @@ -9,6 +9,7 @@ func Name(ctx context.Context, req Request) (Response, error) {} See https://tbd54566975.github.io/ftl/docs/reference/verbs/ --- + type ${1:Request} struct {} type ${2:Response} struct {}