{"record":{"id":"c7ed80827a65e19f","repo":"charmbracelet/crush","slug":"cannot-notify-change-for-unopened-file-s","errorCode":null,"errorMessage":"cannot notify change for unopened file: %s","messagePattern":"cannot notify change for unopened file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/lsp/client.go","lineNumber":446,"sourceCode":"\n\treturn nil\n}\n\n// NotifyChange notifies the server about a file change.\nfunc (c *Client) NotifyChange(ctx context.Context, filepath string) error {\n\tif c == nil {\n\t\treturn nil\n\t}\n\turi := string(protocol.URIFromPath(filepath))\n\n\tcontent, err := os.ReadFile(filepath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading file: %w\", err)\n\t}\n\n\tfileInfo, isOpen := c.openFiles.Get(uri)\n\tif !isOpen {\n\t\treturn fmt.Errorf(\"cannot notify change for unopened file: %s\", filepath)\n\t}\n\n\t// Increment version\n\tfileInfo.Version++\n\n\t// Create change event\n\tchanges := []protocol.TextDocumentContentChangeEvent{\n\t\t{\n\t\t\tValue: protocol.TextDocumentContentChangeWholeDocument{\n\t\t\t\tText: string(content),\n\t\t\t},\n\t\t},\n\t}\n\n\treturn c.client.NotifyDidChangeTextDocument(ctx, uri, int(fileInfo.Version), changes)\n}\n\n// IsFileOpen checks if a file is currently open.","sourceCodeStart":428,"sourceCodeEnd":464,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/lsp/client.go#L428-L464","documentation":"NotifyChange only sends didChange for files the client has previously opened: it looks up the URI in c.openFiles, and if absent returns \"cannot notify change for unopened file\". The LSP protocol requires didOpen before didChange, so the client enforces this ordering and refuses to send a change notification for a document the server has never seen.","triggerScenarios":"Calling NotifyChange on a filepath that was never passed to OpenFile, or after the client restarted (Restart clears openFiles) while callers keep notifying changes; calling with a path whose computed URI differs from the one used at open time.","commonSituations":"Fire-and-forget edit pipelines that call notifyLSPs without an earlier OpenFile; after Restart, stale watchers keep firing NotifyChange for files no longer tracked; path casing/symlink differences make the URI lookup miss.","solutions":["Call OpenFile (or OpenFileOnDemand) for the path before the first NotifyChange.","After Restart, re-open all tracked files before resuming change notifications.","In the caller, treat this error by falling back to OpenFile when the file is unopened.","Ensure the same canonical path string is used for both open and change calls (resolve symlinks)."],"exampleFix":"// before\nclient.NotifyChange(ctx, path) // error if never opened\n\n// after\nif err := client.OpenFile(ctx, path); err != nil {\n    return err\n}\nclient.NotifyChange(ctx, path)","handlingStrategy":"fallback","validationCode":"// Track open state yourself and open before changing\nif !openedPaths[path] {\n    if err := client.OpenFile(ctx, path); err != nil {\n        return err\n    }\n    openedPaths[path] = true\n}","typeGuard":"func isUnopenedFileErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"cannot notify change for unopened file\")\n}","tryCatchPattern":"if err := client.NotifyChange(ctx, path); err != nil {\n    if isUnopenedFileErr(err) {\n        // fallback: open then notify\n        if oerr := client.OpenFile(ctx, path); oerr != nil {\n            return oerr\n        }\n        return client.NotifyChange(ctx, path)\n    }\n    return err\n}","preventionTips":["Always pair didOpen with subsequent didChange calls in your edit pipeline.","After Restart, rebuild your open-file set before resuming edits.","Use one canonical, symlink-resolved path for both open and change calls."],"tags":["lsp","diddchange","state-management"],"backgroundTag":"file-not-open-in-lsp","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}