charmbracelet/crush · error

cannot notify change for unopened file: %s

Error message

cannot notify change for unopened file: %s

What it means

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.

Source

Thrown at internal/lsp/client.go:446

	return nil
}

// NotifyChange notifies the server about a file change.
func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
	if c == nil {
		return nil
	}
	uri := string(protocol.URIFromPath(filepath))

	content, err := os.ReadFile(filepath)
	if err != nil {
		return fmt.Errorf("error reading file: %w", err)
	}

	fileInfo, isOpen := c.openFiles.Get(uri)
	if !isOpen {
		return fmt.Errorf("cannot notify change for unopened file: %s", filepath)
	}

	// Increment version
	fileInfo.Version++

	// Create change event
	changes := []protocol.TextDocumentContentChangeEvent{
		{
			Value: protocol.TextDocumentContentChangeWholeDocument{
				Text: string(content),
			},
		},
	}

	return c.client.NotifyDidChangeTextDocument(ctx, uri, int(fileInfo.Version), changes)
}

// IsFileOpen checks if a file is currently open.

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Call OpenFile (or OpenFileOnDemand) for the path before the first NotifyChange.
  2. After Restart, re-open all tracked files before resuming change notifications.
  3. In the caller, treat this error by falling back to OpenFile when the file is unopened.
  4. Ensure the same canonical path string is used for both open and change calls (resolve symlinks).

Example fix

// before
client.NotifyChange(ctx, path) // error if never opened

// after
if err := client.OpenFile(ctx, path); err != nil {
    return err
}
client.NotifyChange(ctx, path)
Defensive patterns

Strategy: fallback

Validate before calling

// Track open state yourself and open before changing
if !openedPaths[path] {
    if err := client.OpenFile(ctx, path); err != nil {
        return err
    }
    openedPaths[path] = true
}

Type guard

func isUnopenedFileErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "cannot notify change for unopened file")
}

Try / catch

if err := client.NotifyChange(ctx, path); err != nil {
    if isUnopenedFileErr(err) {
        // fallback: open then notify
        if oerr := client.OpenFile(ctx, path); oerr != nil {
            return oerr
        }
        return client.NotifyChange(ctx, path)
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/c7ed80827a65e19f. Report an issue: GitHub.